diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 615222321bca0..ec7ddb4085f24 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ For more detailed information on contribution please read our [beginners guide]( ## Contribution requirements -1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.4/coding-standards/bk-coding-standards.html). +1. Contributions must adhere to the [Magento coding standards](https://developer.adobe.com/commerce/php/coding-standards/). 2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. 3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/HEAD/.github/PULL_REQUEST_TEMPLATE.md) for more information. 4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. diff --git a/README.md b/README.md index 55af19302871e..a02a955a9ebbe 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ However, for those who need a full-featured eCommerce solution, we recommend [Ad ## Contribute -Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://devdocs.magento.com/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas. +Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://developer.adobe.com/commerce/docs/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas. - [Contribute to the code](https://developer.adobe.com/commerce/contributor/guides/code-contributions/) - [Report an issue](https://developer.adobe.com/commerce/contributor/guides/code-contributions/#report) @@ -36,7 +36,7 @@ Our [Community](https://opensource.magento.com/) is large and diverse, and our p ### Maintainers -We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production. +We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production. - [Learn more about the Maintainer role](https://developer.adobe.com/commerce/contributor/guides/maintainers/) - [Maintainer's Handbook](https://developer.adobe.com/commerce/contributor/guides/maintainers/handbook/) @@ -64,9 +64,9 @@ Stay up-to-date on the latest security news and patches by signing up for [Secur ## Licensing Each Magento source file included in this distribution is licensed under OSL 3.0 or the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento). - + [Open Software License (OSL 3.0)](https://opensource.org/licenses/osl-3.0.php) – Please see [LICENSE.txt](LICENSE.txt) for the full text of the OSL 3.0 license. - + Subject to Licensee's/Customer's payment of fees and compliance with the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento), the terms and conditions of the applicable ordering between Licensee/Customer and Adobe (or Magento) supersede the OSL 3.0 license for each source file. ## Communications diff --git a/app/bootstrap.php b/app/bootstrap.php index 8fbe2f770f53b..a7aea8094f816 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -17,12 +17,12 @@ if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80100) { if (PHP_SAPI == 'cli') { echo 'Magento supports PHP 8.1.0 or later. ' . - 'Please read https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements-tech.html'; + 'Please read https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/system-requirements.html'; } else { echo <<

Magento supports PHP 8.1.0 or later. Please read - + Magento System Requirements. HTML; diff --git a/app/code/Magento/AdminAnalytics/README.md b/app/code/Magento/AdminAnalytics/README.md index e905344031ad3..65a9e159f7aea 100644 --- a/app/code/Magento/AdminAnalytics/README.md +++ b/app/code/Magento/AdminAnalytics/README.md @@ -1 +1 @@ -The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin. \ No newline at end of file +The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin. diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml index eb24b074bbffd..f3db9f1fd2636 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml @@ -19,6 +19,7 @@ + diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml index 1ab277b4f788a..53daccc815e50 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml index 8cdbaf781ed05..16a63d65d53c0 100644 --- a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml index b992c84814a29..66423b4210215 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml index c5abff11e2849..97f36e4d66b81 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml @@ -19,6 +19,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml index 6090b5594a671..dd1d1e11cab7e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml index 9f07adb0223a9..6e88124f6f22c 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml index 0df1a6809ccab..7b58b14681143 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -35,7 +35,7 @@ - + diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index badad120fdcca..a0df3f4229a7e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml new file mode 100644 index 0000000000000..c42fd98111e44 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml index c19e102e5d5e8..dd72cd0daa5e4 100644 --- a/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml +++ b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php index 6f908a4c5b218..e7bd6d99cb3eb 100644 --- a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php +++ b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php @@ -6,13 +6,13 @@ namespace Magento\AsynchronousOperations\Model; -/** - * Class AccessValidator - */ +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Authorization\Model\UserContextInterface; + class AccessValidator { /** - * @var \Magento\Authorization\Model\UserContextInterface + * @var UserContextInterface */ private $userContext; @@ -27,13 +27,12 @@ class AccessValidator private $bulkSummaryFactory; /** - * AccessValidator constructor. - * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param UserContextInterface $userContext * @param \Magento\Framework\EntityManager\EntityManager $entityManager * @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory */ public function __construct( - \Magento\Authorization\Model\UserContextInterface $userContext, + UserContextInterface $userContext, \Magento\Framework\EntityManager\EntityManager $entityManager, \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory ) { @@ -50,11 +49,15 @@ public function __construct( */ public function isAllowed($bulkUuid) { - /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ + /** @var BulkSummaryInterface $bulkSummary */ $bulkSummary = $this->entityManager->load( $this->bulkSummaryFactory->create(), $bulkUuid ); + if ((int) $bulkSummary->getUserType() === UserContextInterface::USER_TYPE_INTEGRATION) { + return true; + } + return ((int) $bulkSummary->getUserId()) === ((int) $this->userContext->getUserId()); } } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php index 47c317138ec64..58cc92e649ebf 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php @@ -7,32 +7,29 @@ use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; -/** - * Class Options - */ class Options implements \Magento\Framework\Data\OptionSourceInterface { /** - * @return array + * @inheritDoc */ public function toOptionArray() { return [ [ 'value' => BulkSummaryInterface::NOT_STARTED, - 'label' => 'Not Started' + 'label' => __('Not Started') ], [ 'value' => BulkSummaryInterface::IN_PROGRESS, - 'label' => 'In Progress' + 'label' => __('In Progress') ], [ 'value' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, - 'label' => 'Finished Successfully' + 'label' => __('Finished Successfully') ], [ 'value' => BulkSummaryInterface::FINISHED_WITH_FAILURE, - 'label' => 'Finished with Failure' + 'label' => __('Finished with Failure') ] ]; } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php new file mode 100644 index 0000000000000..92dd301608c18 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php @@ -0,0 +1,31 @@ + UserContextInterface::USER_TYPE_ADMIN, + 'label' => __('Admin user') + ], + [ + 'value' => UserContextInterface::USER_TYPE_INTEGRATION, + 'label' => __('Integration') + ] + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php index 5f2fbd9ea8b11..3fbd93344792d 100644 --- a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php @@ -8,15 +8,13 @@ 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\Model\ResourceModel\AbstractResource; use Psr\Log\LoggerInterface as Logger; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Bulk\BulkSummaryInterface; use Magento\AsynchronousOperations\Model\StatusMapper; use Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql; -/** - * Class SearchResult - */ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult { /** @@ -40,7 +38,6 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi private $calculatedStatusSql; /** - * SearchResult constructor. * @param EntityFactory $entityFactory * @param Logger $logger * @param FetchStrategy $fetchStrategy @@ -49,7 +46,7 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi * @param StatusMapper $statusMapper * @param CalculatedStatusSql $calculatedStatusSql * @param string $mainTable - * @param null $resourceModel + * @param AbstractResource $resourceModel * @param string $identifierName * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -80,7 +77,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _initSelect() { @@ -93,12 +90,18 @@ protected function _initSelect() )->where( 'user_id=?', $this->userContext->getUserId() + )->where( + 'user_type=?', + UserContextInterface::USER_TYPE_ADMIN + )->orWhere( + 'user_type=?', + UserContextInterface::USER_TYPE_INTEGRATION ); return $this; } /** - * {@inheritdoc} + * @inheritdoc */ protected function _afterLoad() { @@ -110,7 +113,7 @@ protected function _afterLoad() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToFilter($field, $condition = null) { @@ -133,7 +136,7 @@ public function addFieldToFilter($field, $condition = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelectCountSql() { diff --git a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv index 44cc0a0ab7754..8a2fc7f774a25 100644 --- a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv +++ b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv @@ -33,3 +33,10 @@ Error,Error "Dismiss All Completed Tasks","Dismiss All Completed Tasks" "Action Details - #","Action Details - #" "Number of Records Affected","Number of Records Affected" +"User Type","User Type" +"Admin user","Admin user" +"Integration","Integration" +"Not Started","Not Started" +"In Progress","In Progress" +"Finished Successfully","Finished Successfully" +"Finished with Failure","Finished with Failure" diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml index 87dc0525eb1c0..981e7ae980102 100644 --- a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml @@ -81,6 +81,14 @@ + + + select + + select + + + select diff --git a/app/code/Magento/Authorization/Model/CompositeUserContext.php b/app/code/Magento/Authorization/Model/CompositeUserContext.php index 149c33f861b35..1ad01a96af20d 100644 --- a/app/code/Magento/Authorization/Model/CompositeUserContext.php +++ b/app/code/Magento/Authorization/Model/CompositeUserContext.php @@ -7,6 +7,7 @@ namespace Magento\Authorization\Model; use Magento\Framework\ObjectManager\Helper\Composite as CompositeHelper; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * User context. @@ -17,7 +18,7 @@ * @api * @since 100.0.2 */ -class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface +class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface, ResetAfterRequestInterface { /** * @var UserContextInterface[] @@ -92,4 +93,12 @@ protected function getUserContext() } return $this->chosenUserContext; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->chosenUserContext = null; + } } diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index def5088e89326..07657d373c95a 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -257,7 +257,7 @@ public function deleteDirectory($path): bool /** * @inheritDoc */ - public function filePutContents($path, $content, $mode = null): int + public function filePutContents($path, $content, $mode = null): bool|int { $path = $this->normalizeRelativePath($path, true); $config = self::CONFIG; @@ -272,10 +272,11 @@ public function filePutContents($path, $content, $mode = null): int try { $this->adapter->write($path, $content, new Config($config)); - return $this->adapter->fileSize($path)->fileSize(); + return ($this->adapter->fileSize($path)->fileSize() !== null)??true; + } catch (FlysystemFilesystemException | UnableToRetrieveMetadata $e) { $this->logger->error($e->getMessage()); - return 0; + return false; } } diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 66c95e97ace0c..b1cec93ed8f7e 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -54,6 +54,11 @@ class AwsS3Factory implements DriverFactoryInterface */ private $cachePrefix; + /** + * @var CachedCredentialsProvider + */ + private $cachedCredentialsProvider; + /** * @param ObjectManagerInterface $objectManager * @param Config $config @@ -61,6 +66,7 @@ class AwsS3Factory implements DriverFactoryInterface * @param CacheInterfaceFactory $cacheInterfaceFactory * @param CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory * @param string|null $cachePrefix + * @param CachedCredentialsProvider|null $cachedCredentialsProvider */ public function __construct( ObjectManagerInterface $objectManager, @@ -68,7 +74,8 @@ public function __construct( MetadataProviderInterfaceFactory $metadataProviderFactory, CacheInterfaceFactory $cacheInterfaceFactory, CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory, - string $cachePrefix = null + string $cachePrefix = null, + ?CachedCredentialsProvider $cachedCredentialsProvider = null, ) { $this->objectManager = $objectManager; $this->config = $config; @@ -76,6 +83,8 @@ public function __construct( $this->cacheInterfaceFactory = $cacheInterfaceFactory; $this->cachedAdapterInterfaceFactory = $cachedAdapterInterfaceFactory; $this->cachePrefix = $cachePrefix; + $this->cachedCredentialsProvider = $cachedCredentialsProvider ?? + $this->objectManager->get(CachedCredentialsProvider::class); } /** @@ -94,18 +103,19 @@ public function create(): RemoteDriverInterface } /** - * @inheritDoc + * Prepare config for S3Client + * + * @param array $config + * @return array + * @throws DriverException */ - public function createConfigured( - array $config, - string $prefix, - string $cacheAdapter = '', - array $cacheConfig = [] - ): RemoteDriverInterface { + private function prepareConfig(array $config) + { $config['version'] = 'latest'; if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) { - unset($config['credentials']); + //Access keys were not provided; request token from AWS config (local or EC2) and cache result + $config['credentials'] = $this->cachedCredentialsProvider->get(); } if (empty($config['bucket']) || empty($config['region'])) { @@ -120,6 +130,19 @@ public function createConfigured( $config['use_path_style_endpoint'] = boolval($config['path_style']); } + return $config; + } + + /** + * @inheritDoc + */ + public function createConfigured( + array $config, + string $prefix, + string $cacheAdapter = '', + array $cacheConfig = [] + ): RemoteDriverInterface { + $config = $this->prepareConfig($config); $client = new S3Client($config); $adapter = new AwsS3V3Adapter($client, $config['bucket'], $prefix); $cache = $this->cacheInterfaceFactory->create( diff --git a/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php new file mode 100644 index 0000000000000..0137284358bd4 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php @@ -0,0 +1,42 @@ +magentoCacheAdapter = $magentoCacheAdapter; + } + + /** + * Provides cache mechanism to retrieve and store AWS credentials + * + * @return callable + */ + public function get() + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return call_user_func( + [CredentialProvider::class, 'cache'], + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func([CredentialProvider::class, 'defaultProvider']), + $this->magentoCacheAdapter + ); + } +} diff --git a/app/code/Magento/AwsS3/Driver/CredentialsCache.php b/app/code/Magento/AwsS3/Driver/CredentialsCache.php new file mode 100644 index 0000000000000..337e9d2a2acff --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/CredentialsCache.php @@ -0,0 +1,82 @@ +magentoCache = $magentoCache; + $this->credentialsFactory = $credentialsFactory; + $this->json = $json; + } + + /** + * @inheritdoc + */ + public function get($key) + { + $value = $this->magentoCache->load($key); + + if (!is_string($value)) { + return null; + } + + $result = $this->json->unserialize($value); + try { + return $this->credentialsFactory->create($result); + } catch (\Exception $e) { + return $result; + } + } + + /** + * @inheritdoc + */ + public function set($key, $value, $ttl = 0) + { + if (method_exists($value, 'toArray')) { + $value = $value->toArray(); + } + $this->magentoCache->save($this->json->serialize($value), $key, [], $ttl); + } + + /** + * @inheritdoc + */ + public function remove($key) + { + $this->magentoCache->remove($key); + } +} diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml new file mode 100644 index 0000000000000..8ce9e8d72db1a --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + <description value="Verifies that zero byte files are synced to AWS S3 with error."/> + <severity value="CRITICAL"/> + <testCaseId value="AC-8252"/> + <useCaseId value="ACP2E-1608"/> + <group value="remote_storage_aws_s3"/> + <group value="skip_in_cloud_native_s3"/> + <group value="remote_storage_disabled"/> + </annotations> + + <before> + <!-- Enable AWS S3 Remote Storage & Sync --> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + <!-- Copy Images to Import Directory for Product Images --> + <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="copy" stepKey="copyProductBaseImage"> + <argument name="source">dev/tests/acceptance/tests/_data/empty.jpg</argument> + <argument name="destination">pub/media/empty.jpg</argument> + </helper> + </before> + + <after> + <!-- Delete Images on Local File System --> + <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteFileIfExists" stepKey="deleteLocalImage"> + <argument name="filePath">pub/media/empty.jpg</argument> + </helper> + <!-- Delete Images on S3 System --> + <helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="deleteFileIfExists" stepKey="deleteS3Image"> + <argument name="filePath">pub/media/empty.jpg</argument> + </helper> + <!-- Disable AWS S3 Remote Storage --> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + <magentoCLI command="remote-storage:sync" timeout="120" stepKey="syncRemoteStorage"/> + <assertEquals stepKey="assertConfigTest"> + <expectedResult type="string">Uploading media files to remote storage.\n- empty.jpg\nEnd of upload.</expectedResult> + <actualResult type="variable">$syncRemoteStorage</actualResult> + </assertEquals> + + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php new file mode 100644 index 0000000000000..48ff824528d26 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Magento\AwsS3\Driver\AwsS3Factory; +use Magento\AwsS3\Driver\CachedCredentialsProvider; +use Magento\Framework\ObjectManagerInterface; +use Magento\RemoteStorage\Driver\Adapter\Cache\CacheInterfaceFactory; +use Magento\RemoteStorage\Driver\Adapter\CachedAdapterInterfaceFactory; +use Magento\RemoteStorage\Driver\Adapter\MetadataProviderInterfaceFactory; +use Magento\RemoteStorage\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AwsS3FactoryTest extends TestCase +{ + /** + * @var AwsS3Factory + */ + private $factory; + + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var Config|MockObject + */ + private $remoteStorageConfigMock; + + /** + * @var MetadataProviderInterfaceFactory|MockObject + */ + private $metadataFactoryMock; + + /** + * @var CacheInterfaceFactory|MockObject + */ + private $remoteStorageCacheMock; + + /** + * @var CachedAdapterInterfaceFactory|MockObject + */ + private $remoteCacheAdapterMock; + + /** + * @var string|null + */ + private $cachePrefix = 'testPrefix'; + + /** + * @var CachedCredentialsProvider|MockObject + */ + private $cachedCredsProviderMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->remoteStorageConfigMock = $this->createMock(Config::class); + $this->metadataFactoryMock = $this->createMock(MetadataProviderInterfaceFactory::class); + $this->remoteStorageCacheMock = $this->createMock(CacheInterfaceFactory::class); + $this->remoteCacheAdapterMock = $this->createMock(CachedAdapterInterfaceFactory::class); + $this->cachedCredsProviderMock = $this->createMock(CachedCredentialsProvider::class); + + $this->factory = new AwsS3Factory( + $this->objectManagerMock, + $this->remoteStorageConfigMock, + $this->metadataFactoryMock, + $this->remoteStorageCacheMock, + $this->remoteCacheAdapterMock, + $this->cachePrefix, + $this->cachedCredsProviderMock + ); + } + + /** + * If no credentials in magento config, credentials retrieved from AWS should be cached + * + * @return void + */ + public function testPrepareConfigUseCache() + { + $config = [ + 'region' => 'us-west-1', + 'bucket' => 'someName', + 'credentials' => [] + ]; + $this->cachedCredsProviderMock->expects($this->once())->method('get'); + $this->invokePrepareConfig($config); + } + + public function testPrepareConfigMissingRequired() + { + $config = [ + 'credentials' => [ + 'key' => 'someKey', + 'secret' => 'verySecretKey' + ] + ]; + + $this->expectException('\Magento\RemoteStorage\Driver\DriverException'); + $this->invokePrepareConfig($config); + } + + /** + * Invoke private method via reflection + * + * @param array $config + * @return array + */ + private function invokePrepareConfig(array $config): array + { + $method = new \ReflectionMethod( + AwsS3Factory::class, + 'prepareConfig' + ); + $method->setAccessible(true); + + return $method->invokeArgs($this->factory, [$config]); + } +} diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php new file mode 100644 index 0000000000000..f5c9b3138ea7f --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Aws\Credentials\CredentialsFactory; +use Magento\AwsS3\Driver\CredentialsCache; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\Serializer\Json; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CredentialsCacheTest extends TestCase +{ + /** + * @var CredentialsCache + */ + private $adapter; + + /** + * @var CacheInterface|MockObject + */ + private $cacheMock; + + /** + * @var CredentialsFactory|MockObject + */ + private $credentialsFactory; + + /** + * @var Json|MockObject + */ + private $jsonMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->cacheMock = $this->createMock(CacheInterface::class); + $this->credentialsFactory = + $this->getMockBuilder(CredentialsFactory::class)->disableOriginalConstructor()->getMock(); + $this->jsonMock = $this->createMock(Json::class); + $this->adapter = new CredentialsCache($this->cacheMock, $this->credentialsFactory, $this->jsonMock); + } + + public function testSet() + { + $this->jsonMock->expects($this->once())->method('serialize')->with('value')->willReturn('serialized'); + $this->cacheMock->expects($this->once())->method('save')->with('serialized', 'key'); + $this->adapter->set('key', 'value'); + } + + public function testGetEmpty() + { + $this->cacheMock->expects($this->once())->method('load')->with('key'); + $actual = $this->adapter->get('key'); + $this->assertEquals(null, $actual); + } + + public function testRemove() + { + $this->cacheMock->expects($this->once())->method('remove'); + $this->adapter->remove('key'); + } +} diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php index 3d7154eb20f92..11cca3717ba20 100644 --- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php +++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Block\System\Store\Grid\Render; +use Magento\Framework\DataObject; + /** * Store render group * @@ -13,9 +15,9 @@ class Group extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** - * {@inheritdoc} + * @inheritDoc */ - public function render(\Magento\Framework\DataObject $row) + public function render(DataObject $row) { if (!$row->getData($this->getColumn()->getIndex())) { return null; @@ -28,6 +30,6 @@ public function render(\Magento\Framework\DataObject $row) '">' . $this->escapeHtml($row->getData($this->getColumn()->getIndex())) . '</a><br />' - . '(' . __('Code') . ': ' . $row->getGroupCode() . ')'; + . '(' . __('Code') . ': ' . $this->escapeHtml($row->getGroupCode()) . ')'; } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php index 74117fbd666cc..66777b3968436 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php @@ -109,25 +109,16 @@ public function getHtml() ' value="' . $this->localeResolver->getLocale() . '"/>'; - $scriptString = ' - require(["jquery", "mage/calendar"], function($){ - $("#' . - $htmlId . - '_range").dateRange({ - dateFormat: "' . - $format . - '", - buttonText: "' . $this->escapeHtml(__('Date selector')) . - '", + $scriptString = 'require(["jquery", "mage/calendar"], function($){ + $("#' . $htmlId . '_range").dateRange({ + dateFormat: "' . $format . '", + buttonText: "' . $this->escapeHtml(__('Date selector')) . '", + buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '", from: { - id: "' . - $htmlId . - '_from" + id: "' . $htmlId . '_from" }, to: { - id: "' . - $htmlId . - '_to" + id: "' . $htmlId . '_to" } }) });'; diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index a139d20191b57..c0c01c6201ce0 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -17,7 +17,7 @@ class Datetime extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Date /** * full day is 86400, we need 23 hours:59 minutes:59 seconds = 86399 */ - const END_OF_DAY_IN_SECONDS = 86399; + public const END_OF_DAY_IN_SECONDS = 86399; /** * @inheritdoc @@ -123,6 +123,7 @@ public function getHtml() timeFormat: "' . $timeFormat . '", showsTime: ' . ($this->getColumn()->getFilterTime() ? 'true' : 'false') . ', buttonText: "' . $this->escapeHtml(__('Date selector')) . '", + buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '", from: { id: "' . $htmlId . '_from" }, diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php index 0da7e4db9b983..b7928eb027454 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php @@ -15,6 +15,7 @@ * * @api * @deprecated 100.2.0 in favour of UI component implementation + * @see don't recommend this approach in favour of UI component implementation * @since 100.0.2 */ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text @@ -132,7 +133,7 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) } if (empty($action['id'])) { - $action['id'] = 'id' .$this->random->getRandomString(10); + $action['id'] = 'id' . $this->random->getRandomString(10); } $actionAttributes->setData($action); $onclick = $actionAttributes->getData('onclick'); @@ -140,6 +141,8 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) $actionAttributes->unsetData(['onclick', 'style']); $html = '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; if ($onclick) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $onclick = html_entity_decode($onclick); $html .= $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onclick, "#{$action['id']}"); } if ($style) { diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php index 22ca8a49c155b..5b180ba7f7de9 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php @@ -1067,10 +1067,11 @@ public function getCsv() $data = []; foreach ($this->getColumns() as $column) { if (!$column->getIsSystem()) { + $exportField = (string)$column->getRowFieldExport($item); $data[] = '"' . str_replace( ['"', '\\'], ['""', '\\\\'], - $column->getRowFieldExport($item) ?: '' + $exportField ?: '' ) . '"'; } } diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index e65caf13f2ea3..ec813472695d7 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -114,6 +114,16 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_isFirstAfterLogin = null; + $this->acl = null; + } + /** * Refresh ACL resources stored in session * diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index ed0312874565c..b3067d3c98851 100644 --- a/app/code/Magento/Backend/Model/Session/Quote.php +++ b/app/code/Magento/Backend/Model/Session/Quote.php @@ -139,6 +139,17 @@ public function __construct( } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_quote = null; + $this->_store = null; + $this->_order = null; + } + /** * Retrieve quote model object * @@ -154,7 +165,7 @@ public function getQuote() $this->_quote->setCustomerGroupId($customerGroupId); $this->_quote->setIsActive(false); $this->_quote->setStoreId($this->getStoreId()); - + $this->quoteRepository->save($this->_quote); $this->setQuoteId($this->_quote->getId()); $this->_quote = $this->quoteRepository->get($this->getQuoteId(), [$this->getStoreId()]); diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml index 3d596d248c42b..025255088d7e6 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml @@ -14,7 +14,8 @@ </annotations> <arguments> <argument name="username" type="string" defaultValue="{{_ENV.MAGENTO_ADMIN_USERNAME}}"/> - <argument name="password" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/></arguments> + <argument name="password" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/> + </arguments> <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{username}}" stepKey="fillUsername"/> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml index cd6eca91e9e30..b62a3d868d0d1 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml @@ -27,8 +27,10 @@ <waitForPageLoad stepKey="waitForTaxRateLoad"/> <!-- delete the rule --> + <waitForElementVisible selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="waitForDelete"/> <click stepKey="clickDelete" selector="{{AdminStoresMainActionsSection.deleteButton}}"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmationModal"/> <click stepKey="clickOk" selector="{{AdminConfirmationModalSection.ok}}"/> - <see stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/> + <waitForText stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml index 440c73bc73a91..371c8dfbb8bf7 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml @@ -20,7 +20,7 @@ <waitForElementVisible selector="{{AdminSystemAccountSection.interfaceLocale}}" stepKey="waitForInterfaceLocale"/> <!-- Change Admin locale to Français (France) / French (France) --> <selectOption userInput="{{InterfaceLocaleByValue}}" selector="{{AdminSystemAccountSection.interfaceLocale}}" stepKey="setInterfaceLocate"/> - <fillField selector="{{AdminSystemAccountSection.currentPassword}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <fillField selector="{{AdminSystemAccountSection.currentPassword}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the account." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml index 5aaefc383f413..268ff07850f43 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -16,5 +16,10 @@ <element name="localeDisabled" type="select" selector="#general_locale_code[disabled=disabled]"/> <element name="useDefault" type="checkbox" selector="#general_locale_timezone_inherit"/> <element name="defaultLocale" type="checkbox" selector="#general_locale_code_inherit"/> + <element name="checkIfTabExpand" type="button" selector="#general_locale-head:not(.open)"/> + <element name="timeZoneDropdown" type="select" selector="//select[@id='general_locale_timezone']"/> + <element name="changeStoreConfigButton" type="button" selector="//button[@id='store-change-button']"/> + <element name="changeStoreConfigToSpecificWebsite" type="select" selector="//a[contains(text(),'{{var}}')]" parameterized="true"/> + <element name="changeWebsiteConfirmButton" type="button" selector="//button[@class='action-primary action-accept']/span"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml index f7c5ae308c755..7bd1bf85a37fc 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-96409"/> <group value="backend"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml index 88660b74cd6f9..1b1f1deada589 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml index b4f44ea9e0a6e..1b80a1d897ccf 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml index eaf3fd240417d..4babc8a266b85 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-17275"/> <group value="backend"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{ChangedCookieDomainForMainWebsiteConfigData.path}} --scope={{ChangedCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{ChangedCookieDomainForMainWebsiteConfigData.scope_code}} {{ChangedCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteBeforeTestRun"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml index f27d02c751945..a40e6f474c1ca 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{MinifyJavaScriptFilesEnableConfigData.path}} {{MinifyJavaScriptFilesEnableConfigData.value}}" stepKey="enableJsMinification"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml index 8c3ebd96f502e..61f8ee9461941 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml @@ -19,10 +19,11 @@ <group value="example"/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"> - <argument name="password" value="INVALID!{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <argument name="password" value="INVALID!"/> </actionGroup> <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="assertErrorMessage"/> </test> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml index 73b14bdc14151..915b00e9189d7 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-71572"/> <group value="example"/> <group value="login"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml index b19981f78df60..98c6c01053ffd 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml @@ -20,6 +20,7 @@ <group value="example"/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml index e5b92f61230b4..06a433e17ae6f 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml @@ -18,6 +18,7 @@ <description value="Check login with restrict role."/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml index 45a49f58788fc..5d4f1a4f31487 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-95349"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml index c4cbfcfaa12b2..6ad97b44999b0 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <testCaseId value="MC-27441"/> <group value="Admin_UI"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml index d4ec0829604bb..133be0bd74bbd 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17787"/> <group value="backend"/> <group value="login"/> + <group value="cloud"/> </annotations> <!-- Logging in Magento admin and checking for Privacy policy footer in dashboard --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml index 664067a66d20e..1afd625167740 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -18,6 +18,7 @@ <group value="backend"/> <group value="search"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml index 22b45210fe6ba..d9aa8c2850c69 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml index b1b780190a699..d45171890e782 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml index d69ceeba29d18..452517b460652 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml index 44c230e271a19..961e08b21efb9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml index a6510c5d82717..b931d606a9da1 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27191"/> <severity value="MAJOR"/> <group value="reorder"/> + <group value="cloud"/> </annotations> <!-- Log in as admin--> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php index 575403824679f..0a387b31bf8e7 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php @@ -10,18 +10,21 @@ use Magento\Backend\Block\Context; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Column\Filter\Date; +use Magento\Framework\App\Request\Http; use Magento\Framework\Escaper; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Math\Random; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Asset\Repository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Class DateTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DateTest extends TestCase { @@ -49,6 +52,16 @@ class DateTest extends TestCase /** @var Context|MockObject */ private $contextMock; + /** + * @var Http|MockObject + */ + private $request; + + /** + * @var Repository|MockObject + */ + private $repositoryMock; + protected function setUp(): void { $this->mathRandomMock = $this->getMockBuilder(Random::class) @@ -88,6 +101,23 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->request); + + $this->repositoryMock = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->setMethods(['getUrlWithParams']) + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getAssetRepository') + ->willReturn($this->repositoryMock); + $objectManagerHelper = new ObjectManager($this); $this->model = $objectManagerHelper->getObject( Date::class, @@ -116,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp() 'from' => $yesterday->getTimestamp(), 'to' => $tomorrow->getTimestamp() ]; + $params = ['_secure' => false]; + $fileId = 'Magento_Theme::calendar.png'; + $fileUrl = 'file url'; + + $this->repositoryMock->expects($this->once()) + ->method('getUrlWithParams') + ->with($fileId, $params) + ->willReturn($fileUrl); $this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash); $this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id); diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php index 3296680f43374..4b63e34cbc879 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php @@ -10,17 +10,21 @@ use Magento\Backend\Block\Context; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Column\Filter\Datetime; +use Magento\Framework\App\Request\Http; use Magento\Framework\Escaper; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Math\Random; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Asset\Repository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Class DateTimeTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DatetimeTest extends TestCase { @@ -48,6 +52,16 @@ class DatetimeTest extends TestCase /** @var Context|MockObject */ private $contextMock; + /** + * @var Http|MockObject + */ + private $request; + + /** + * @var Repository|MockObject + */ + private $repositoryMock; + protected function setUp(): void { $this->mathRandomMock = $this->getMockBuilder(Random::class) @@ -87,6 +101,23 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->request); + + $this->repositoryMock = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->setMethods(['getUrlWithParams']) + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getAssetRepository') + ->willReturn($this->repositoryMock); + $objectManagerHelper = new ObjectManager($this); $this->model = $objectManagerHelper->getObject( Datetime::class, @@ -115,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp() 'from' => $yesterday->getTimestamp(), 'to' => $tomorrow->getTimestamp() ]; + $params = ['_secure' => false]; + $fileId = 'Magento_Theme::calendar.png'; + $fileUrl = 'file url'; + + $this->repositoryMock->expects($this->once()) + ->method('getUrlWithParams') + ->with($fileId, $params) + ->willReturn($fileUrl); $this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash); $this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id); diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 463976b58212f..1610ea9fde71f 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -349,9 +349,10 @@ <field id="transport">smtp</field> </depends> </field> - <field id="password" translate="label comment" type="password" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="password" translate="label comment" type="obscure" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Password</label> - <comment>Username</comment> + <comment>Password</comment> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <depends> <field id="transport">smtp</field> </depends> diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php index 864e5f4b37721..252ca89ae411b 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php @@ -6,9 +6,10 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; -class Download extends \Magento\Backup\Controller\Adminhtml\Index +class Download extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -66,17 +67,12 @@ public function execute() $fileName = $this->_objectManager->get(\Magento\Backup\Helper\Data::class)->generateBackupDownloadName($backup); - $this->_fileFactory->create( + return $this->_fileFactory->create( $fileName, - null, + ['type' => 'filename', 'value' => $backup->getPath() . DIRECTORY_SEPARATOR . $backup->getFileName()], DirectoryList::VAR_DIR, 'application/octet-stream', $backup->getSize() ); - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($backup->output()); - return $resultRaw; } } diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php index c38a7b3005e21..cf39406d54aeb 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Db.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php @@ -301,7 +301,7 @@ public function rollBackTransaction() */ public function runCommand($command) { - $this->connection->query($command); + $this->connection->multiQuery($command); return $this; } } diff --git a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php index 4ae20c711327f..b9c6b67cf1bc7 100644 --- a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php +++ b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php @@ -115,7 +115,7 @@ protected function setUp(): void ->getMock(); $this->backupModelMock = $this->getMockBuilder(Backup::class) ->disableOriginalConstructor() - ->setMethods(['getTime', 'exists', 'getSize', 'output']) + ->setMethods(['getTime', 'exists', 'getSize', 'output', 'getPath', 'getFileName']) ->getMock(); $this->dataHelperMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() @@ -169,8 +169,13 @@ public function testExecuteBackupFound() $type = 'db'; $filename = 'filename'; $size = 10; - $output = 'test'; - + $path = 'testpath'; + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getFileName') + ->willReturn($filename); $this->backupModelMock->expects($this->atLeastOnce()) ->method('getTime') ->willReturn($time); @@ -180,9 +185,6 @@ public function testExecuteBackupFound() $this->backupModelMock->expects($this->atLeastOnce()) ->method('getSize') ->willReturn($size); - $this->backupModelMock->expects($this->atLeastOnce()) - ->method('output') - ->willReturn($output); $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap( @@ -206,20 +208,14 @@ public function testExecuteBackupFound() $this->fileFactoryMock->expects($this->once()) ->method('create')->with( $filename, - null, + ['type' => 'filename', 'value' => $path . '/' . $filename], DirectoryList::VAR_DIR, 'application/octet-stream', $size ) ->willReturn($this->responseMock); - $this->resultRawMock->expects($this->once()) - ->method('setContents') - ->with($output); - $this->resultRawFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->resultRawMock); - $this->assertSame($this->resultRawMock, $this->downloadController->execute()); + $this->assertSame($this->responseMock, $this->downloadController->execute()); } /** diff --git a/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php new file mode 100644 index 0000000000000..921f871da7265 --- /dev/null +++ b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Api; + +/** + * Interface for Bulk children addition + */ +interface ProductLinkManagementAddChildrenInterface +{ + /** + * Bulk add children operation + * + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param int $optionId + * @param \Magento\Bundle\Api\Data\LinkInterface[] $linkedProducts + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @return void + */ + public function addChildren( + \Magento\Catalog\Api\Data\ProductInterface $product, + int $optionId, + array $linkedProducts + ); +} diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php index 8f89910558c97..8c4f19193e225 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -19,6 +19,7 @@ use Magento\Framework\DataObject; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Stdlib\ArrayUtils; /** @@ -28,7 +29,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Bundle extends AbstractView +class Bundle extends AbstractView implements ResetAfterRequestInterface { /** * @var array @@ -36,8 +37,6 @@ class Bundle extends AbstractView protected $options; /** - * Catalog product - * * @var \Magento\Catalog\Helper\Product */ protected $catalogProduct; @@ -405,7 +404,7 @@ private function getConfigData(Product $product, array $options) */ private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues) { - $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? []; + $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/{$optionId}") ?? []; $selections = $options[$optionId]['selections']; array_walk( $selections, @@ -423,4 +422,13 @@ function (&$selection, $selectionId) use ($preConfiguredQtys) { return $options; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->selectedOptions = []; + $this->optionsPosition = []; + } } diff --git a/app/code/Magento/Bundle/Model/LinkManagement.php b/app/code/Magento/Bundle/Model/LinkManagement.php index 9bc056a3e9a87..3569a026144b8 100644 --- a/app/code/Magento/Bundle/Model/LinkManagement.php +++ b/app/code/Magento/Bundle/Model/LinkManagement.php @@ -10,6 +10,7 @@ use Magento\Bundle\Api\Data\LinkInterface; use Magento\Bundle\Api\Data\LinkInterfaceFactory; use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface; use Magento\Bundle\Api\ProductLinkManagementInterface; use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Bundle; @@ -30,7 +31,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class LinkManagement implements ProductLinkManagementInterface +class LinkManagement implements ProductLinkManagementInterface, ProductLinkManagementAddChildrenInterface { /** * @var ProductRepositoryInterface @@ -189,6 +190,70 @@ public function saveChild( return true; } + /** + * Linked product processing + * + * @param LinkInterface $linkedProduct + * @param array $selections + * @param int $optionId + * @param ProductInterface $product + * @param string $linkField + * @param Bundle $resource + * @return int + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + */ + private function processLinkedProduct( + LinkInterface $linkedProduct, + array $selections, + int $optionId, + ProductInterface $product, + string $linkField, + Bundle $resource + ): int { + $linkProductModel = $this->productRepository->get($linkedProduct->getSku()); + if ($linkProductModel->isComposite()) { + throw new InputException(__('The bundle product can\'t contain another composite product.')); + } + + if ($selections) { + foreach ($selections as $selection) { + if ($selection['option_id'] == $optionId && + $selection['product_id'] == $linkProductModel->getEntityId() && + $selection['parent_product_id'] == $product->getData($linkField)) { + if (!$product->getCopyFromView()) { + throw new CouldNotSaveException( + __( + 'Child with specified sku: "%1" already assigned to product: "%2"', + [$linkedProduct->getSku(), $product->getSku()] + ) + ); + } + } + } + } + + $selectionModel = $this->bundleSelection->create(); + $selectionModel = $this->mapProductLinkToBundleSelectionModel( + $selectionModel, + $linkedProduct, + $product, + (int)$linkProductModel->getEntityId() + ); + + $selectionModel->setOptionId($optionId); + + try { + $selectionModel->save(); + $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId()); + } catch (\Exception $e) { + throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e); + } + + return (int)$selectionModel->getId(); + } + /** * Fill selection model with product link data * @@ -196,12 +261,11 @@ public function saveChild( * @param LinkInterface $productLink * @param string $linkedProductId * @param string $parentProductId - * * @return Selection - * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @deprecated use mapProductLinkToBundleSelectionModel + * @deprecated + * @see mapProductLinkToBundleSelectionModel */ protected function mapProductLinkToSelectionModel( Selection $selectionModel, @@ -246,9 +310,9 @@ protected function mapProductLinkToSelectionModel( * @param LinkInterface $productLink * @param ProductInterface $parentProduct * @param int $linkedProductId - * @param string $linkField * @return Selection * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function mapProductLinkToBundleSelectionModel( Selection $selectionModel, @@ -290,8 +354,6 @@ private function mapProductLinkToBundleSelectionModel( /** * @inheritDoc - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function addChild( ProductInterface $product, @@ -325,49 +387,52 @@ public function addChild( /* @var $resource Bundle */ $resource = $this->bundleFactory->create(); $selections = $resource->getSelectionsData($product->getData($linkField)); - /** @var Product $linkProductModel */ - $linkProductModel = $this->productRepository->get($linkedProduct->getSku()); - if ($linkProductModel->isComposite()) { - throw new InputException(__('The bundle product can\'t contain another composite product.')); - } - - if ($selections) { - foreach ($selections as $selection) { - if ($selection['option_id'] == $optionId && - $selection['product_id'] == $linkProductModel->getEntityId() && - $selection['parent_product_id'] == $product->getData($linkField)) { - if (!$product->getCopyFromView()) { - throw new CouldNotSaveException( - __( - 'Child with specified sku: "%1" already assigned to product: "%2"', - [$linkedProduct->getSku(), $product->getSku()] - ) - ); - } - - return $this->bundleSelection->create()->load($linkProductModel->getEntityId()); - } - } - } - - $selectionModel = $this->bundleSelection->create(); - $selectionModel = $this->mapProductLinkToBundleSelectionModel( - $selectionModel, + return $this->processLinkedProduct( $linkedProduct, + $selections, + (int)$optionId, $product, - (int)$linkProductModel->getEntityId() + $linkField, + $resource ); + } - $selectionModel->setOptionId($optionId); + /** + * @inheritDoc + */ + public function addChildren( + ProductInterface $product, + int $optionId, + array $linkedProducts + ) : void { + if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) { + throw new InputException( + __('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku()) + ); + } - try { - $selectionModel->save(); - $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId()); - } catch (\Exception $e) { - throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $options = $this->optionCollection->create(); + $options->setIdFilter($optionId); + $options->setProductLinkFilter($product->getData($linkField)); + $existingOption = $options->getFirstItem(); + + if (!$existingOption->getId()) { + throw new InputException( + __( + 'Product with specified sku: "%1" does not contain option: "%2"', + [$product->getSku(), $optionId] + ) + ); } - return (int)$selectionModel->getId(); + /* @var $resource Bundle */ + $resource = $this->bundleFactory->create(); + $selections = $resource->getSelectionsData($product->getData($linkField)); + + foreach ($linkedProducts as $linkedProduct) { + $this->processLinkedProduct($linkedProduct, $selections, $optionId, $product, $linkField, $resource); + } } /** diff --git a/app/code/Magento/Bundle/Model/Option/SaveAction.php b/app/code/Magento/Bundle/Model/Option/SaveAction.php index 0fe0f7d97ea07..2776f11db33f8 100644 --- a/app/code/Magento/Bundle/Model/Option/SaveAction.php +++ b/app/code/Magento/Bundle/Model/Option/SaveAction.php @@ -7,20 +7,26 @@ namespace Magento\Bundle\Model\Option; +use Exception; use Magento\Bundle\Api\Data\LinkInterface; use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface; +use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Option; +use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Bundle\Model\Product\Type; -use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; /** * Encapsulates logic for saving a bundle option, including coalescing the parent product's data. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveAction { @@ -44,12 +50,18 @@ class SaveAction */ private $linkManagement; + /** + * @var ProductLinkManagementAddChildrenInterface + */ + private $addChildren; + /** * @param Option $optionResource * @param MetadataPool $metadataPool * @param Type $type * @param ProductLinkManagementInterface $linkManagement * @param StoreManagerInterface|null $storeManager + * @param ProductLinkManagementAddChildrenInterface|null $addChildren * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -57,41 +69,68 @@ public function __construct( MetadataPool $metadataPool, Type $type, ProductLinkManagementInterface $linkManagement, - ?StoreManagerInterface $storeManager = null + ?StoreManagerInterface $storeManager = null, + ?ProductLinkManagementAddChildrenInterface $addChildren = null ) { $this->optionResource = $optionResource; $this->metadataPool = $metadataPool; $this->type = $type; $this->linkManagement = $linkManagement; + $this->addChildren = $addChildren ?: + ObjectManager::getInstance()->get(ProductLinkManagementAddChildrenInterface::class); } /** - * Manage the logic of saving a bundle option, including the coalescence of its parent product data. + * Bulk options save * * @param ProductInterface $bundleProduct - * @param OptionInterface $option - * @return OptionInterface + * @param OptionInterface[] $options + * @return void * @throws CouldNotSaveException - * @throws \Exception + * @throws NoSuchEntityException + * @throws InputException */ - public function save(ProductInterface $bundleProduct, OptionInterface $option) + public function saveBulk(ProductInterface $bundleProduct, array $options): void { $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $optionCollection = $this->type->getOptionsCollection($bundleProduct); + + foreach ($options as $option) { + $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata); + } + + $bundleProduct->setIsRelationsChanged(true); + } + + /** + * Process option save + * + * @param ProductInterface $bundleProduct + * @param OptionInterface $option + * @param Collection $optionCollection + * @param EntityMetadataInterface $metadata + * @return void + * @throws CouldNotSaveException + * @throws NoSuchEntityException + * @throws InputException + */ + private function saveOptionItem( + ProductInterface $bundleProduct, + OptionInterface $option, + Collection $optionCollection, + EntityMetadataInterface $metadata + ) : void { + $linksToAdd = []; $option->setStoreId($bundleProduct->getStoreId()); $parentId = $bundleProduct->getData($metadata->getLinkField()); $option->setParentId($parentId); - $optionId = $option->getOptionId(); - $linksToAdd = []; - $optionCollection = $this->type->getOptionsCollection($bundleProduct); /** @var \Magento\Bundle\Model\Option $existingOption */ $existingOption = $optionCollection->getItemById($option->getOptionId()) ?? $optionCollection->getNewEmptyItem(); if (!$optionId || $existingOption->getParentId() != $parentId) { - //If option ID is empty or existing option's parent ID is different - //we'd need a new ID for the option. $option->setOptionId(null); $option->setDefaultTitle($option->getTitle()); if (is_array($option->getProductLinks())) { @@ -110,7 +149,7 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option) try { $this->optionResource->save($option); - } catch (\Exception $e) { + } catch (Exception $e) { throw new CouldNotSaveException(__("The option couldn't be saved."), $e); } @@ -118,8 +157,23 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option) foreach ($linksToAdd as $linkedProduct) { $this->linkManagement->addChild($bundleProduct, $option->getOptionId(), $linkedProduct); } + } - $bundleProduct->setIsRelationsChanged(true); + /** + * Manage the logic of saving a bundle option, including the coalescence of its parent product data. + * + * @param ProductInterface $bundleProduct + * @param OptionInterface $option + * @return OptionInterface + * @throws CouldNotSaveException + * @throws Exception + */ + public function save(ProductInterface $bundleProduct, OptionInterface $option) + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $optionCollection = $this->type->getOptionsCollection($bundleProduct); + + $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata); return $option; } @@ -149,6 +203,7 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac } /** @var LinkInterface[] $linksToDelete */ $linksToDelete = $this->compareLinks($existingLinks, $linksToUpdate); + $linksToUpdate = $this->verifyLinksToUpdate($existingLinks, $linksToUpdate); } foreach ($linksToUpdate as $linkedProduct) { $this->linkManagement->saveChild($product->getSku(), $linkedProduct); @@ -160,9 +215,56 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac $linkedProduct->getSku() ); } - foreach ($linksToAdd as $linkedProduct) { - $this->linkManagement->addChild($product, $option->getOptionId(), $linkedProduct); + $this->addChildren->addChildren($product, (int)$option->getOptionId(), $linksToAdd); + } + + /** + * Verify that updated data actually changed + * + * @param LinkInterface[] $existing + * @param LinkInterface[] $updates + * @return array + */ + private function verifyLinksToUpdate(array $existing, array $updates) : array + { + $linksToUpdate = []; + $beforeLinksMap = []; + + foreach ($existing as $beforeLink) { + $beforeLinksMap[$beforeLink->getId()] = $beforeLink; + } + + foreach ($updates as $updatedLink) { + if (array_key_exists($updatedLink->getId(), $beforeLinksMap)) { + $beforeLink = $beforeLinksMap[$updatedLink->getId()]; + if ($this->isLinkChanged($beforeLink, $updatedLink)) { + $linksToUpdate[] = $updatedLink; + } + } else { + $linksToUpdate[] = $updatedLink; + } } + return $linksToUpdate; + } + + /** + * Check is updated link actually updated + * + * @param LinkInterface $beforeLink + * @param LinkInterface $updatedLink + * @return bool + */ + private function isLinkChanged(LinkInterface $beforeLink, LinkInterface $updatedLink) : bool + { + return (int)$beforeLink->getOptionId() !== (int)$updatedLink->getOptionId() + || $beforeLink->getIsDefault() !== $updatedLink->getIsDefault() + || (float)$beforeLink->getQty() !== (float)$updatedLink->getQty() + || $beforeLink->getPrice() !== $updatedLink->getPrice() + || $beforeLink->getCanChangeQuantity() !== $updatedLink->getCanChangeQuantity() + || (array)$beforeLink->getExtensionAttributes() !== (array)$updatedLink->getExtensionAttributes() + || (int)$beforeLink->getPosition() !== (int)$updatedLink->getPosition() + || $beforeLink->getSku() !== $updatedLink->getSku() + || $beforeLink->getPriceType() !== $updatedLink->getPriceType(); } /** diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php index 2f6708a17639e..b7260dd49b20b 100644 --- a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php +++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php @@ -9,11 +9,12 @@ use Magento\Bundle\Model\Product\Type as BundleType; use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Add child identities to product identities on storefront. */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var BundleType @@ -68,4 +69,12 @@ private function getChildrenIds($entityId): array return $this->cacheChildrenIds[$entityId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheChildrenIds = []; + } } diff --git a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php index 42c6930469ac9..ff5b902c37e74 100644 --- a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php +++ b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php @@ -9,11 +9,12 @@ use Magento\Bundle\Model\Product\Type as BundleType; use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Add parent identities to product identities. */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var BundleType @@ -68,4 +69,12 @@ private function getParentIdsByChild($entityId): array return $this->cacheParentIdsByChild[$entityId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheParentIdsByChild = []; + } } diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index e536b8961ebb9..412783565486e 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -103,9 +103,7 @@ public function execute($entity, $arguments = []) $existingOptionsIds = !empty($existingBundleProductOptions) ? $this->getOptionIds($existingBundleProductOptions) : []; - $optionIds = !empty($bundleProductOptions) - ? $this->getOptionIds($bundleProductOptions) - : []; + $optionIds = $this->getOptionIds($bundleProductOptions); if (!$entity->getCopyFromView()) { $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); @@ -161,12 +159,11 @@ protected function removeOptionLinks($entitySku, $option) private function saveOptions($entity, array $options, array $newOptionsIds = []): void { foreach ($options as $option) { - if (in_array($option->getOptionId(), $newOptionsIds, true)) { + if (in_array($option->getOptionId(), $newOptionsIds)) { $option->setOptionId(null); } - - $this->optionSave->save($entity, $option); } + $this->optionSave->saveBulk($entity, $options); } /** @@ -184,7 +181,7 @@ private function getOptionIds(array $options): array /** @var OptionInterface $option */ foreach ($options as $option) { if ($option->getOptionId()) { - $optionIds[] = $option->getOptionId(); + $optionIds[] = (int)$option->getOptionId(); } } } diff --git a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php index d3f1c2f1c9991..424330a1671eb 100644 --- a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php +++ b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php @@ -10,6 +10,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Magento\Catalog\Model\Product; @@ -18,7 +19,7 @@ /** * Class to return ids of options and child products when all products in required option are disabled in bundle product */ -class SelectionProductsDisabledRequired +class SelectionProductsDisabledRequired implements ResetAfterRequestInterface { /** * @var BundleSelection @@ -161,4 +162,12 @@ private function getCacheKey(int $bundleId, int $websiteId): string { return $bundleId . '-' . $websiteId; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productsDisabledRequired = []; + } } diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index d542458c365a7..511dbd092bcfe 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -7,6 +7,7 @@ namespace Magento\Bundle\Model\Product; use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\ResourceModel\Option\AreBundleOptionsSalable; use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; @@ -170,6 +171,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $arrayUtility; + /** + * @var AreBundleOptionsSalable + */ + private $areBundleOptionsSalable; + /** * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig @@ -196,7 +202,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier * @param ArrayUtils|null $arrayUtility - * @param UploaderFactory $uploaderFactory + * @param UploaderFactory|null $uploaderFactory + * @param AreBundleOptionsSalable|null $areBundleOptionsSalable * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -225,7 +232,8 @@ public function __construct( MetadataPool $metadataPool = null, SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, ArrayUtils $arrayUtility = null, - UploaderFactory $uploaderFactory = null + UploaderFactory $uploaderFactory = null, + AreBundleOptionsSalable $areBundleOptionsSalable = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -246,6 +254,8 @@ public function __construct( $this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier ?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class); $this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class); + $this->areBundleOptionsSalable = $areBundleOptionsSalable + ?? ObjectManager::getInstance()->get(AreBundleOptionsSalable::class); parent::__construct( $catalogProductOption, @@ -595,44 +605,8 @@ public function isSalable($product) return $product->getData('all_items_salable'); } - $metadata = $this->metadataPool->getMetadata( - \Magento\Catalog\Api\Data\ProductInterface::class - ); - - $isSalable = false; - foreach ($this->getOptionsCollection($product) as $option) { - $hasSalable = false; - - $selectionsCollection = $this->_bundleCollection->create(); - $selectionsCollection->addAttributeToSelect('status'); - $selectionsCollection->addQuantityFilter(); - $selectionsCollection->setFlag('product_children', true); - $selectionsCollection->addFilterByRequiredOptions(); - $selectionsCollection->setOptionIdsFilter([$option->getId()]); - - $this->selectionCollectionFilterApplier->apply( - $selectionsCollection, - 'parent_product_id', - $product->getData($metadata->getLinkField()) - ); - - foreach ($selectionsCollection as $selection) { - if ($selection->isSalable()) { - $hasSalable = true; - break; - } - } - - if ($hasSalable) { - $isSalable = true; - } - - if (!$hasSalable && $option->getRequired()) { - $isSalable = false; - break; - } - } - + $store = $this->_storeManager->getStore(); + $isSalable = $this->areBundleOptionsSalable->execute((int) $product->getEntityId(), (int) $store->getId()); $product->setData('all_items_salable', $isSalable); return $isSalable; diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php index c322a4b26241d..1562620fe03a2 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php @@ -40,6 +40,8 @@ public function __construct( } /** + * Build bundle options select + * * @param string $idxTable * @return Select */ @@ -67,6 +69,10 @@ public function buildSelect($idxTable) ['i' => $idxTable], 'i.product_id = bs.product_id AND i.website_id = cis.website_id AND i.stock_id = cis.stock_id', [] + )->joinLeft( + ['cisi' => $this->resourceConnection->getTableName('cataloginventory_stock_item')], + 'cisi.product_id = i.product_id AND cisi.stock_id = i.stock_id', + [] )->joinLeft( ['e' => $this->resourceConnection->getTableName('catalog_product_entity')], 'e.entity_id = bs.product_id', diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php index b3c3e74e1fa60..1730c3b62d3bf 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php @@ -15,11 +15,12 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Remove bundle product from price index when all products in required option are disabled */ -class DisabledProductOptionPriceModifier implements PriceModifierInterface +class DisabledProductOptionPriceModifier implements PriceModifierInterface, ResetAfterRequestInterface { /** * @var ResourceConnection @@ -145,4 +146,12 @@ private function getBundleIds(array $entityIds): \Traversable yield $id; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->websiteIdsOfProduct = []; + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php index 6808081506dd7..173e8f257b063 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php @@ -92,12 +92,7 @@ protected function _prepareBundleOptionStockData($entityIds = null, $usePrimaryT $idxTable = $usePrimaryTable ? $table : $this->getIdxTable(); $select = $this->bundleOptionStockDataSelectBuilder->buildSelect($idxTable); - $status = new \Zend_Db_Expr( - 'MAX(' - . $connection->getCheckSql('e.required_options = 0', 'i.stock_status', '0') - . ')' - ); - + $status = $this->getOptionsStatusExpression(); $select->columns(['status' => $status]); if ($entityIds !== null) { @@ -194,4 +189,49 @@ protected function _cleanBundleOptionStockData() $this->getConnection()->delete($this->_getBundleOptionTable()); return $this; } + + /** + * Build expression for bundle options stock status + * + * @return \Zend_Db_Expr + */ + private function getOptionsStatusExpression(): \Zend_Db_Expr + { + $connection = $this->getConnection(); + $isAvailableExpr = $connection->getCheckSql( + 'bs.selection_can_change_qty = 0 AND bs.selection_qty > i.qty', + '0', + 'i.stock_status' + ); + if ($this->stockConfiguration->getBackorders()) { + $backordersExpr = $connection->getCheckSql( + 'cisi.use_config_backorders = 0 AND cisi.backorders = 0', + $isAvailableExpr, + 'i.stock_status' + ); + } else { + $backordersExpr = $connection->getCheckSql( + 'cisi.use_config_backorders = 0 AND cisi.backorders > 0', + 'i.stock_status', + $isAvailableExpr + ); + } + if ($this->stockConfiguration->getManageStock()) { + $statusExpr = $connection->getCheckSql( + 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 0', + 1, + $backordersExpr + ); + } else { + $statusExpr = $connection->getCheckSql( + 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 1', + $backordersExpr, + 1 + ); + } + + return new \Zend_Db_Expr( + 'MAX(' . $connection->getCheckSql('e.required_options = 0', $statusExpr, '0') . ')' + ); + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php new file mode 100644 index 0000000000000..bfea7dc1295c5 --- /dev/null +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\ResourceModel\Option; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; + +class AreBundleOptionsSalable +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $productAttributeRepository + */ + public function __construct( + ResourceConnection $resourceConnection, + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $productAttributeRepository + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->productAttributeRepository = $productAttributeRepository; + } + + /** + * Check are bundle product options salable + * + * @param int $entityId + * @param int $storeId + * @return bool + */ + public function execute(int $entityId, int $storeId): bool + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $connection = $this->resourceConnection->getConnection(); + $optionsSaleabilitySelect = $connection->select() + ->from( + ['parent_products' => $this->resourceConnection->getTableName('catalog_product_entity')], + [] + )->joinInner( + ['bundle_options' => $this->resourceConnection->getTableName('catalog_product_bundle_option')], + "bundle_options.parent_id = parent_products.{$linkField}", + [] + )->joinInner( + ['bundle_selections' => $this->resourceConnection->getTableName('catalog_product_bundle_selection')], + 'bundle_selections.option_id = bundle_options.option_id', + [] + )->joinInner( + ['child_products' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'child_products.entity_id = bundle_selections.product_id', + [] + )->group( + ['bundle_options.parent_id', 'bundle_options.option_id'] + )->where( + 'parent_products.entity_id = ?', + $entityId + ); + $statusAttr = $this->productAttributeRepository->get(ProductInterface::STATUS); + $optionsSaleabilitySelect->joinInner( + ['child_status_global' => $statusAttr->getBackendTable()], + "child_status_global.{$linkField} = child_products.{$linkField}" + . " AND child_status_global.attribute_id = {$statusAttr->getAttributeId()}" + . " AND child_status_global.store_id = 0", + [] + )->joinLeft( + ['child_status_store' => $statusAttr->getBackendTable()], + "child_status_store.{$linkField} = child_products.{$linkField}" + . " AND child_status_store.attribute_id = {$statusAttr->getAttributeId()}" + . " AND child_status_store.store_id = {$storeId}", + [] + ); + $isOptionSalableExpr = new \Zend_Db_Expr( + sprintf( + 'MAX(IFNULL(child_status_store.value, child_status_global.value) != %s)', + ProductStatus::STATUS_DISABLED + ) + ); + $isRequiredOptionUnsalable = $connection->getCheckSql( + 'required = 1 AND ' . $isOptionSalableExpr . ' = 0', + '1', + '0' + ); + $optionsSaleabilitySelect->columns([ + 'required' => 'bundle_options.required', + 'is_salable' => $isOptionSalableExpr, + 'is_required_and_unsalable' => $isRequiredOptionUnsalable, + ]); + + $select = $connection->select()->from( + $optionsSaleabilitySelect, + [new \Zend_Db_Expr('(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)')] + ); + $isSalable = $connection->fetchOne($select); + + return (bool) $isSalable; + } +} diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 303c33b571d35..04f4305bdf77d 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -128,7 +128,6 @@ public function __construct( $metadataPool, $tableMaintainer ); - $this->stockItem = $stockItem ?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); } @@ -145,6 +144,17 @@ protected function _construct() $this->_selectionTable = $this->getTable('catalog_product_bundle_selection'); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->itemPrototype = null; + $this->catalogRuleProcessor = null; + $this->websiteScopePriceJoined = false; + } + /** * Set store id for each collection item when collection was loaded. * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod @@ -355,8 +365,6 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false) * Get Catalog Rule Processor. * * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor - * - * @deprecated 100.2.0 */ private function getCatalogRuleProcessor() { diff --git a/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php new file mode 100644 index 0000000000000..3475eb5cd3d85 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php @@ -0,0 +1,235 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Sales\Order; + +use Magento\Bundle\Model\Sales\Order\Shipment\BundleShipmentTypeValidator; +use \Laminas\Validator\ValidatorInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Phrase; +use Magento\Framework\Webapi\Request; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\Order\Shipment; + +/** + * Validate if requested order items can be shipped according to bundle product shipment type + */ +class BundleOrderTypeValidator extends BundleShipmentTypeValidator implements ValidatorInterface +{ + private const SHIPMENT_API_ROUTE = 'v1/shipment'; + + public const SHIPMENT_TYPE_TOGETHER = '0'; + + public const SHIPMENT_TYPE_SEPARATELY = '1'; + + /** + * @var array + */ + private array $messages = []; + + /** + * @var Request + */ + private Request $request; + + /** + * @param Request $request + */ + public function __construct(Request $request) + { + $this->request = $request; + } + + /** + * Validates shipment items based on order item properties + * + * @param Shipment $value + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Sales\Exception\DocumentValidationException + */ + public function isValid($value): bool + { + if (false === $this->validationNeeded()) { + return true; + } + + $result = $shippingInfo = []; + foreach ($value->getItems() as $shipmentItem) { + $shippingInfo[$shipmentItem->getOrderItemId()] = [ + 'shipment_info' => $shipmentItem, + 'order_info' => $value->getOrder()->getItemById($shipmentItem->getOrderItemId()) + ]; + } + + foreach ($shippingInfo as $shippingItemInfo) { + if ($shippingItemInfo['order_info']->getProductType() === Type::TYPE_BUNDLE) { + $result[] = $this->checkBundleItem($shippingItemInfo, $shippingInfo); + } elseif ($shippingItemInfo['order_info']->getParentItem() && + $shippingItemInfo['order_info']->getParentItem()->getProductType() === Type::TYPE_BUNDLE + ) { + $result[] = $this->checkChildItem($shippingItemInfo['order_info'], $shippingInfo); + } + $this->renderValidationMessages($result); + } + + return empty($this->messages); + } + + /** + * Returns validation messages + * + * @return array|string[] + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * Checks if shipment child item can be processed + * + * @param Item $orderItem + * @param array $shipmentInfo + * @return Phrase|null + * @throws NoSuchEntityException + */ + private function checkChildItem(Item $orderItem, array $shipmentInfo): ?Phrase + { + $result = null; + if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE && + $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + '%3 should be shipped instead.', + $orderItem->getParentItem()->getSku(), + __('Together'), + __('Bundle product itself') + ); + } + + if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE && + $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY && + false === $this->hasParentInShipping($orderItem, $shipmentInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product %1 should be included as well.', + $orderItem->getParentItem()->getSku() + ); + } + + return $result; + } + + /** + * Checks if bundle item can be processed as a shipment item + * + * @param array $shippingItemInfo + * @param array $shippingInfo + * @return Phrase|null + */ + private function checkBundleItem(array $shippingItemInfo, array $shippingInfo): ?Phrase + { + $result = null; + /** @var Item $orderItem */ + $orderItem = $shippingItemInfo['order_info']; + /** @var ShipmentItemInterface $shipmentItem */ + $shipmentItem = $shippingItemInfo['shipment_info']; + + if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER && + $this->hasChildrenInShipping($shipmentItem, $shippingInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + '%3 should be shipped instead.', + $orderItem->getSku(), + __('Together'), + __('Bundle product itself') + ); + } + if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY && + false === $this->hasChildrenInShipping($shipmentItem, $shippingInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + 'Shipment should also incorporate bundle options.', + $orderItem->getSku(), + __('Separately') + ); + } + return $result; + } + + /** + * Determines if a child shipment item has its corresponding parent in shipment + * + * @param Item $childItem + * @param array $shipmentInfo + * @return bool + */ + private function hasParentInShipping(Item $childItem, array $shipmentInfo): bool + { + /** @var Item $orderItem */ + foreach (array_column($shipmentInfo, 'order_info') as $orderItem) { + if (!$orderItem->getParentItemId() && + $orderItem->getProductType() === Type::TYPE_BUNDLE && + $childItem->getParentItemId() == $orderItem->getItemId() + ) { + return true; + } + } + return false; + } + + /** + * Determines if a bundle shipment item has at least one child in shipment + * + * @param ShipmentItemInterface $bundleItem + * @param array $shippingInfo + * @return bool + */ + private function hasChildrenInShipping(ShipmentItemInterface $bundleItem, array $shippingInfo): bool + { + /** @var Item $orderItem */ + foreach (array_column($shippingInfo, 'order_info') as $orderItem) { + if ($orderItem->getParentItemId() && + $orderItem->getParentItemId() == $bundleItem->getOrderItemId() + ) { + return true; + } + } + return false; + } + + /** + * Determines if the validation should be triggered or not + * + * @return bool + */ + private function validationNeeded(): bool + { + return str_contains(strtolower($this->request->getUri()->getPath()), self::SHIPMENT_API_ROUTE); + } + + /** + * Creates text based validation messages + * + * @param array $validationMessages + * @return void + */ + private function renderValidationMessages(array $validationMessages): void + { + foreach ($validationMessages as $message) { + if ($message instanceof Phrase) { + $this->messages[] = $message->render(); + } + } + $this->messages = array_unique($this->messages); + } +} diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php index 3051394eaf512..5e38edcb37607 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php @@ -12,6 +12,7 @@ use Magento\Bundle\Pricing\Price\BundleSelectionFactory; use Magento\Bundle\Pricing\Price\BundleSelectionPrice; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Adjustment\Calculator as CalculatorBase; use Magento\Framework\Pricing\Amount\AmountFactory; use Magento\Framework\Pricing\Amount\AmountInterface; @@ -25,7 +26,7 @@ * Bundle price calculator * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Calculator implements BundleCalculatorInterface +class Calculator implements BundleCalculatorInterface, ResetAfterRequestInterface { /** * @var CalculatorBase @@ -214,7 +215,8 @@ protected function getSelectionAmounts(Product $bundleProduct, $searchMin, $useR * @param Option $option * @param bool $canSkipRequiredOption * @return bool - * @deprecated 100.2.0 + * @deprecated 100.2.0 Not used anymore. + * @see Nothing */ protected function canSkipOption($option, $canSkipRequiredOption) { @@ -226,7 +228,8 @@ protected function canSkipOption($option, $canSkipRequiredOption) * * @param Product $bundleProduct * @return bool - * @deprecated 100.2.0 + * @deprecated 100.2.0 Not used anymore. + * @see Nothing */ protected function hasRequiredOption($bundleProduct) { @@ -245,6 +248,7 @@ function ($item) { * @param Product $saleableItem * @return \Magento\Bundle\Model\ResourceModel\Option\Collection * @deprecated 100.2.0 + * @see Nothing */ protected function getBundleOptions(Product $saleableItem) { @@ -425,4 +429,12 @@ public function processOptions($option, $selectionPriceList, $searchMin = true) } return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionAmount = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php index 5d9e703c2414c..361eac3062341 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php @@ -11,13 +11,14 @@ use Magento\Catalog\Model\Product; use Magento\Bundle\Model\Product\Price; use Magento\Catalog\Helper\Data as CatalogData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; /** * Provide lightweight implementation which uses price index */ -class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface +class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface, ResetAfterRequestInterface { /** * @var BundleSelectionFactory @@ -245,4 +246,12 @@ private function getBundleOptions(Product $saleableItem) { return $saleableItem->getTypeInstance()->getOptionsCollection($saleableItem); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->priceList = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php index e4951cc311737..4ac7bdd798e36 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php @@ -8,6 +8,7 @@ namespace Magento\Bundle\Pricing\Price; use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Catalog\Model\Product; @@ -15,7 +16,7 @@ /** * Bundle option price calculation model. */ -class BundleOptions +class BundleOptions implements ResetAfterRequestInterface { /** * @var BundleCalculatorInterface @@ -91,6 +92,7 @@ public function calculateOptions( /** @var \Magento\Bundle\Pricing\Price\BundleSelectionPrice $selectionPriceList */ $selectionPriceList = $this->calculator->createSelectionPriceList($option, $bundleProduct); $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $priceList = array_merge($priceList, $selectionPriceList); } $amount = $this->calculator->calculateBundleAmount(0., $bundleProduct, $priceList); @@ -135,4 +137,12 @@ public function getOptionSelectionAmount( return $this->optionSelectionAmountCache[$cacheKey]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionSelectionAmountCache = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php index 9bda194df4b0e..5028c35eea008 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php @@ -7,6 +7,8 @@ namespace Magento\Bundle\Pricing\Price; use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Catalog\Pricing\Price\CustomOptionPrice; use Magento\Bundle\Model\Product\Price; @@ -14,7 +16,7 @@ /** * Bundle product regular price model */ -class BundleRegularPrice extends \Magento\Catalog\Pricing\Price\RegularPrice implements RegularPriceInterface +class BundleRegularPrice extends RegularPrice implements RegularPriceInterface, ResetAfterRequestInterface { /** * @var BundleCalculatorInterface @@ -72,4 +74,13 @@ public function getMinimalPrice() { return $this->getAmount(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->maximalPrice = null; + $this->amount = []; + } } diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index e5f557dd22ded..67844e8759bf5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -31,6 +31,11 @@ <data key="fixedPriceFormatted">$10.00</data> <data key="defaultAttribute">Default</data> </entity> + <entity name="BundleProductWithSlashSku" type="product"> + <data key="name">BundleProduct</data> + <data key="sku">bu/ndle</data> + <data key="status">1</data> + </entity> <entity name="FixedBundleProduct" type="product2"> <data key="name" unique="suffix">FixedBundleProduct</data> <data key="sku" unique="suffix">fixed-bundle-product</data> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 8b78ac7b5fe6e..295243f8a81d3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -125,5 +125,7 @@ <element name="priceType" type="select" selector="[name='product[options][0][price_type]']" /> <element name="priceTypeSelectPercent" type="select" selector="//*[@name='product[options][0][price_type]']/option[2]" /> <element name="weightFieldLabel" type="input" selector="//div[@data-index='weight']/div/label/span"/> + <!--Errors--> + <element name="fieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml index 2fde274dc5288..05f6c73f77826 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-223"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml index 9722835b201c1..3cd206f083dce 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17782"/> <useCaseId value="MC-17387"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml new file mode 100644 index 0000000000000..25c94f015a109 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddDecimalDefaultToBundleItemsTest"> + <annotations> + <features value="Bundle"/> + <stories value="Create/Edit bundle product in Admin"/> + <title value="Admin should be able to set decimal default to bundle item when item allows it"/> + <description value="Admin should be able to set decimal default value to new bundle option"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8646"/> + <useCaseId value="ACP2E-1799"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Open simpleProduct1 in Admin --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterSimpleProduct1"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$simpleProduct1.name$$')}}" stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + <!-- Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> + + <!-- Create new Bundle product --> + <actionGroup ref="AdminOpenCreateBundleProductPageActionGroup" stepKey="goToBundleProductCreationPage"/> + <actionGroup ref="AdminClickAddOptionOnBundleProductEditPageActionGroup" stepKey="clickAddOption1"/> + <actionGroup ref="AdminFillBundleOptionTitleActionGroup" stepKey="fillOptionTitle"> + <argument name="optionTitle" value="{{BundleProduct.optionTitle1}}"/> + </actionGroup> + <actionGroup ref="AdminFillBundleOptionTypeActionGroup" stepKey="selectInputType"/> + + <actionGroup ref="AdminClickAddProductToOptionByOptionIndexActionGroup" stepKey="clickAddProductsToOption"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <actionGroup ref="AdminCheckFirstCheckboxInAddProductsToOptionPanelGridActionGroup" stepKey="selectFirstGridRow"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <actionGroup ref="AdminCheckFirstCheckboxInAddProductsToOptionPanelGridActionGroup" stepKey="selectFirstGridRow2"/> + <actionGroup ref="AdminClickAddSelectedProductsOnAddProductsToOptionPanelActionGroup" stepKey="clickAddSelectedBundleProducts"/> + + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" stepKey="grabbedFirstBundleOptionQuantity"/> + <assertEquals stepKey="assertFirstBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedFirstBundleOptionQuantity</actualResult> + </assertEquals> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" stepKey="grabbedSecondBundleOptionQuantity"/> + <assertEquals stepKey="assertSecondBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedSecondBundleOptionQuantity</actualResult> + </assertEquals> + + <!-- Fill first selection with decimal value --> + <actionGroup ref="AdminFillBundleItemQtyActionGroup" stepKey="fillProduct1DefaultQty"> + <argument name="optionIndex" value="0"/> + <argument name="productIndex" value="0"/> + <argument name="qty" value="2.56"/> + </actionGroup> + + <!-- Check there is no error message for the slection with allowed decimal value --> + <dontSee selector="{{AdminProductFormBundleSection.fieldError('uid')}}" userInput="Please enter a valid number in this field." stepKey="doNotSeeErrorMessageForProduct1"/> + + <!-- Fill second selection with decimal value --> + <actionGroup ref="AdminFillBundleItemQtyActionGroup" stepKey="fillProduct2DefaultQty"> + <argument name="optionIndex" value="0"/> + <argument name="productIndex" value="1"/> + <argument name="qty" value="2.56"/> + </actionGroup> + + <!-- Check there is an error message for the slection with not allowed decimal value --> + <see selector="{{AdminProductFormBundleSection.fieldError('uid')}}" userInput="Please enter a valid number in this field." stepKey="seeErrorMessageForProduct2"/> + + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml index 5936948d0a8c2..9c40d19644ff9 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-115"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml index cbba284859697..ed0ba38674184 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-221"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml index d7d053c3e1f54..9538542c75ded 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-222"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml index a41e1f369b707..c1bd617eccce2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml @@ -15,6 +15,7 @@ <description value="create bundle product calculate and Verify price on product page"/> <severity value="MAJOR"/> <testCaseId value="AC-4610"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml index 3edca9a5bb7ff..f37224cd76ced 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-40309"/> <useCaseId value="MC-30152"/> <group value="bundle"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml index 244ea386e7a11..8cd46fb082b1e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml @@ -15,6 +15,7 @@ <description value="Checking Bundle product SKUs in items ordered page"/> <severity value="MAJOR"/> <testCaseId value="AC-3898"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml index 7bcd4d0899ede..f48dcc6fee50a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-224"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a Website --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml index 9d24d4f8d38b6..2a097d105c27c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-27223"/> <severity value="MAJOR"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- creating category, simple products --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml index 2f7dd14d1d712..1333d460b7214 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-216"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml index 467fd965e3282..ae44d098e8e7c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26056"/> <group value="mtf_migrated"/> <group value="bundle"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and simple product --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml index 79c7d113477fb..0d10071fdc991 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11017"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml index b5b0fa3187b03..07b3b7096bd04 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3342"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Admin login--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml index 77a43721d4e67..97c46b07b2e5c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminFilterProductListByBundleProductInDutchUserLanguageTest"> + <test name="AdminFilterProductListByBundleProductInDutchUserLanguageTest"> <annotations> <features value="Bundle"/> <stories value="Admin list bundle products when user language is set as Dutch"/> @@ -42,6 +42,12 @@ <argument name="InterfaceLocaleByValue" value="en_US" /> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Change Admin locale to Nederlands (Nederland) / Nederlands (Nederland) --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml index 5c23360e74d78..3a096a1a6cbd4 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-218"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml index 643e71626e62b..12eb267e3ec8d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-225"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating Data--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index 482c8ed503676..3d2b096249c9a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-200"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml index eadf7667b010b..e1ec035c6f326 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-143"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index 30397d8473550..92db02dd4205c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-186"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating data--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml index c56e09562d49a..fc577bf4dd8e9 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml @@ -13,6 +13,7 @@ <title value="Customer should get the right subtotal in cart when the bundle product with dynamic tier price added to the cart"/> <description value="Customer should be able to add bundle product with dynamic tier price to the cart and get the right price"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml index 1b33bb08b1b03..55d9439a544ef 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml @@ -13,6 +13,7 @@ <title value="Customer should get the right subtotal in cart when the bundle product with tier price for sub-item added to the cart"/> <description value="Customer should be able to add bundle product with tier price for sub-item price to the cart and get the right price"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml index eb047822cd230..0d3c9dbf2d09c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94467"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml index 963f144dfeaad..1633491410233 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml @@ -16,6 +16,7 @@ <description value="Verify that the user is able to checkout bundled product even after one of more selected options are removed from admin"/> <severity value="MAJOR"/> <testCaseId value="AC-4608"/> + <group value="cloud"/> </annotations> <before> <!-- Create Customer Account --> @@ -85,8 +86,8 @@ <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCart"/> - <see selector="{{StorefrontBundledSection.nthItemOptionsValue('1')}}" userInput="1 x $$SimpleProduct1.name$$ $10.00" stepKey="seeOptionValue1"/> - <see selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="1 x $$SimpleProduct2.name$$ $15.00" stepKey="seeOptionValue2"/> + <waitForText selector="{{StorefrontBundledSection.nthItemOptionsValue('1')}}" userInput="1 x $$SimpleProduct1.name$$ $10.00" stepKey="seeOptionValue1"/> + <waitForText selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="1 x $$SimpleProduct2.name$$ $15.00" stepKey="seeOptionValue2"/> <openNewTab stepKey="openNewTab"/> @@ -114,12 +115,12 @@ </actionGroup> <click stepKey="clickEdit" selector="{{CheckoutCartProductSection.nthEditButton('1')}}"/> - + <waitForElementClickable selector="{{StorefrontProductInfoMainSection.updateCart}}" stepKey="waitForUpdateCartButtonClickable" /> <click selector="{{StorefrontProductInfoMainSection.updateCart}}" stepKey="clickUpdateCartButton"/> - + <waitForElementClickable selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitForProceedToCheckoutClickable" /> <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> - <see selector="{{CheckoutHeaderSection.shippingMethodStep}}" userInput="Shipping" stepKey="checkShippingHeader"/> + <waitForText selector="{{CheckoutHeaderSection.shippingMethodStep}}" userInput="Shipping" stepKey="checkShippingHeader"/> </test> </tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml index 8859f9d2a3c86..51f701f251ab3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml @@ -84,7 +84,7 @@ </before> <after> <!--Remove default flat rate shipping method settings--> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> @@ -110,7 +110,7 @@ <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="openBundleProductEditPage"/> <!--Create new customer order.--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <!--Add bundle product to order.--> @@ -134,7 +134,7 @@ <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> <wait time="2" stepKey="waitForPageLoad1"/> <!--Create new customer order.--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> <argument name="customer" value="$createCustomer2$"/> </actionGroup> <!--Add bundle product to order.--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml index 5758a782d3b55..55e3a4106f351 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-215"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating data--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml index f4b81e9ba9577..b6ad937eac4a5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-220"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml index 378c59048cdea..f1ce131d13e38 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95933"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml index 37e743c0dc049..7d8f793883ce3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-291"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml index 55bb27d317c1c..5846190c80bc5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml @@ -18,6 +18,7 @@ <severity value="MINOR"/> <group value="Bundle"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simpleProduct1" before="bundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml index 918e6014dbb97..9961855e93518 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-226"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Admin login--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml index 61545268ef63e..800df1d991389 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-231"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml index 9c334fea8d80a..50fe5c76549de 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-290"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml index 3fa758effc18a..5d710475251d3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-40744"/> <group value="bundle"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 9ab7df0f5dc7a..0933a1ecceb6c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-228"/> <group value="bundle"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml index b486d95ac3e4b..e4739f416ce4a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-42765"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php new file mode 100644 index 0000000000000..1b8fd65455f3d --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Option; + +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Option\SaveAction; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\ResourceModel\Option as OptionResource; +use Magento\Bundle\Model\ResourceModel\Option\Collection; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaveActionTest extends TestCase +{ + /** + * @var Option|MockObject + */ + private $optionResource; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var Type|MockObject + */ + private $type; + + /** + * @var ProductLinkManagementInterface|MockObject + */ + private $linkManagement; + + /** + * @var ProductInterface|MockObject + */ + private $product; + + /** + * @var SaveAction + */ + private $saveAction; + + protected function setUp(): void + { + $this->linkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->type = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionResource = $this->getMockBuilder(OptionResource::class) + ->disableOriginalConstructor() + ->getMock(); + $this->product = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId', 'getData', 'setIsRelationsChanged']) + ->getMockForAbstractClass(); + + $this->saveAction = new SaveAction( + $this->optionResource, + $this->metadataPool, + $this->type, + $this->linkManagement + ); + } + + public function testSaveBulk() + { + $option = $this->getMockBuilder(Option::class) + ->onlyMethods(['getOptionId', 'setData', 'getData']) + ->addMethods(['setStoreId', 'setParentId', 'getParentId']) + ->disableOriginalConstructor() + ->getMock(); + $option->expects($this->any()) + ->method('getOptionId') + ->willReturn(1); + $option->expects($this->any()) + ->method('getData') + ->willReturn([]); + $bundleOptions = [$option]; + + $collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $collection->expects($this->once()) + ->method('getItemById') + ->with(1) + ->willReturn($option); + $this->type->expects($this->once()) + ->method('getOptionsCollection') + ->willReturn($collection); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->linkManagement->expects($this->once()) + ->method('getChildren') + ->willReturn([]); + $this->product->expects($this->once()) + ->method('setIsRelationsChanged') + ->with(true); + + $this->saveAction->saveBulk($this->product, $bundleOptions); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php new file mode 100644 index 0000000000000..3eeac7a324c07 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Product; + +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; +use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Model\Option\SaveAction; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\Product\SaveHandler; +use Magento\Bundle\Model\Product\CheckOptionLinkIfExist; +use Magento\Bundle\Model\ProductRelationsProcessorComposite; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaveHandlerTest extends TestCase +{ + /** + * @var ProductLinkManagementInterface|MockObject + */ + private $productLinkManagement; + + /** + * @var OptionRepository|MockObject + */ + private $optionRepository; + + /** + * @var SaveAction|MockObject + */ + private $optionSave; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CheckOptionLinkIfExist|MockObject + */ + private $checkOptionLinkIfExist; + + /** + * @var ProductRelationsProcessorComposite|MockObject + */ + private $productRelationsProcessorComposite; + + /** + * @var ProductInterface|MockObject + */ + private $entity; + + /** + * @var SaveHandler + */ + private $saveHandler; + + protected function setUp(): void + { + $this->productLinkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionRepository = $this->getMockBuilder(OptionRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionSave = $this->getMockBuilder(SaveAction::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkOptionLinkIfExist = $this->getMockBuilder(CheckOptionLinkIfExist::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productRelationsProcessorComposite = $this->getMockBuilder(ProductRelationsProcessorComposite::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entity = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getCopyFromView', 'getData']) + ->getMockForAbstractClass(); + $this->entity->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_CODE); + + $this->saveHandler = new SaveHandler( + $this->optionRepository, + $this->productLinkManagement, + $this->optionSave, + $this->metadataPool, + $this->checkOptionLinkIfExist, + $this->productRelationsProcessorComposite + ); + } + + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testExecuteWithBulkOptionsProcessing(): void + { + $option = $this->getMockBuilder(OptionInterface::class) + ->onlyMethods(['getOptionId']) + ->getMockForAbstractClass(); + $option->expects($this->any()) + ->method('getOptionId') + ->willReturn(1); + $bundleOptions = [$option]; + + $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + ->addMethods(['getBundleProductOptions']) + ->getMockForAbstractClass(); + $extensionAttributes->expects($this->any()) + ->method('getBundleProductOptions') + ->willReturn($bundleOptions); + $this->entity->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->willReturn($metadata); + $this->optionRepository->expects($this->any()) + ->method('getList') + ->willReturn($bundleOptions); + + $this->optionSave->expects($this->once()) + ->method('saveBulk'); + $this->saveHandler->execute($this->entity); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 68310d1d2bb44..a222b3c3eff3c 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -2129,61 +2129,6 @@ public function testIsSalableFalse(): void $this->assertFalse($this->model->isSalable($product)); } - /** - * @return void - */ - public function testIsSalableWithoutOptions(): void - { - $optionCollectionMock = $this->getOptionCollectionMock([]); - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @return void - */ - public function testIsSalableWithRequiredOptionsTrue(): void - { - $option1 = $this->getRequiredOptionMock(10, 10); - $option2 = $this->getRequiredOptionMock(20, 10); - - $option3 = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) - ->onlyMethods(['getRequired', 'getOptionId', 'getId']) - ->disableOriginalConstructor() - ->getMock(); - $option3->method('getRequired') - ->willReturn(false); - $option3->method('getOptionId') - ->willReturn(30); - $option3->method('getId') - ->willReturn(30); - - $this->expectProductEntityMetadata(); - - $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2, $option3]); - $selectionCollectionMock = $this->getSelectionCollectionMock([$option1, $option2]); - $this->bundleCollectionFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($selectionCollectionMock); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertTrue($this->model->isSalable($product)); - } - /** * @return void */ @@ -2200,124 +2145,6 @@ public function testIsSalableCache(): void $this->assertTrue($this->model->isSalable($product)); } - /** - * @return void - */ - public function testIsSalableWithEmptySelectionsCollection(): void - { - $option = $this->getRequiredOptionMock(1, 10); - $optionCollectionMock = $this->getOptionCollectionMock([$option]); - $selectionCollectionMock = $this->getSelectionCollectionMock([]); - $this->expectProductEntityMetadata(); - - $this->bundleCollectionFactory->expects($this->once()) - ->method('create') - ->willReturn($selectionCollectionMock); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @return void - */ - public function testIsSalableWithNonSalableRequiredOptions(): void - { - $option1 = $this->getRequiredOptionMock(10, 10); - $option2 = $this->getRequiredOptionMock(20, 10); - $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2]); - $this->expectProductEntityMetadata(); - - $selection1 = $this->getMockBuilder(Product::class) - ->onlyMethods(['isSalable']) - ->disableOriginalConstructor() - ->getMock(); - - $selection1->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $selection2 = $this->getMockBuilder(Product::class) - ->onlyMethods(['isSalable']) - ->disableOriginalConstructor() - ->getMock(); - - $selection2->expects($this->once()) - ->method('isSalable') - ->willReturn(false); - - $selectionCollectionMock1 = $this->getSelectionCollectionMock([$selection1]); - $selectionCollectionMock2 = $this->getSelectionCollectionMock([$selection2]); - - $this->bundleCollectionFactory->expects($this->exactly(2)) - ->method('create') - ->will($this->onConsecutiveCalls( - $selectionCollectionMock1, - $selectionCollectionMock2 - )); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @param int $id - * @param int $selectionQty - * - * @return MockObject - */ - private function getRequiredOptionMock(int $id, int $selectionQty): MockObject - { - $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) - ->onlyMethods( - [ - 'getRequired', - 'getOptionId', - 'getId' - ] - ) - ->addMethods( - [ - 'isSalable', - 'hasSelectionQty', - 'getSelectionQty', - 'getSelectionCanChangeQty' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $option->method('getRequired') - ->willReturn(true); - $option->method('isSalable') - ->willReturn(true); - $option->method('hasSelectionQty') - ->willReturn(true); - $option->method('getSelectionQty') - ->willReturn($selectionQty); - $option->method('getOptionId') - ->willReturn($id); - $option->method('getSelectionCanChangeQty') - ->willReturn(false); - $option->method('getId') - ->willReturn($id); - - return $option; - } - /** * @param array $selectedOptions * @@ -2338,25 +2165,6 @@ private function getSelectionCollectionMock(array $selectedOptions): MockObject return $selectionCollectionMock; } - /** - * @param array $options - * - * @return MockObject - */ - private function getOptionCollectionMock(array $options): MockObject - { - $optionCollectionMock = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Option\Collection::class) - ->onlyMethods(['getIterator']) - ->disableOriginalConstructor() - ->getMock(); - - $optionCollectionMock->expects($this->any()) - ->method('getIterator') - ->willReturn(new \ArrayIterator($options)); - - return $optionCollectionMock; - } - /** * @param bool $isManageStock * diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php new file mode 100644 index 0000000000000..a2ac9b109de20 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php @@ -0,0 +1,246 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Sales\Order; + +use Laminas\Uri\Http as HttpUri; +use Magento\Bundle\Model\Sales\Order\BundleOrderTypeValidator; +use Magento\Catalog\Model\Product; +use Magento\Framework\Webapi\Request; +use Magento\Sales\Model\Order\Shipment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Model\Product\Type; + +class BundleOrderTypeValidatorTest extends TestCase +{ + /** + * @var Request|Request&MockObject|MockObject + */ + private Request $request; + + /** + * @var BundleOrderTypeValidator + */ + private BundleOrderTypeValidator $validator; + + /** + * @return void + */ + protected function setUp(): void + { + $this->request = $this->createMock(Request::class); + $uri = $this->createMock(HttpUri::class); + $uri->expects($this->any())->method('getPath')->willReturn('V1/shipment/'); + $this->request->expects($this->any())->method('getUri')->willReturn($uri); + + $this->validator = new BundleOrderTypeValidator($this->request); + + parent::setUp(); + } + + /** + * @return void + */ + public function testIsValidSuccessShipmentTypeTogether(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->once()) + ->method('getItemById') + ->willReturn($bundleOrderItem); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem]); + $shipment->expects($this->once())->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertEmpty($this->validator->getMessages()); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + public function testIsValidSuccessShipmentTypeSeparately() + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem, $childShipmentItem]); + $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertEmpty($this->validator->getMessages()); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return void + */ + public function testIsValidFailSeparateShipmentType(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku'); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + $childOrderItem->expects($this->any())->method('getParentItem')->willReturn($bundleOrderItem); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturn($childOrderItem); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$childShipmentItem]); + $shipment->expects($this->once())->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertNotEmpty($this->validator->getMessages()); + $this->assertTrue( + in_array( + 'Cannot create shipment as bundle product sku should be included as well.', + $this->validator->getMessages() + ) + ); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return void + */ + public function testIsValidFailTogetherShipmentType(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku'); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem, $childShipmentItem]); + $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertNotEmpty($this->validator->getMessages()); + $this->assertTrue( + in_array( + 'Cannot create shipment as bundle product "sku" has shipment type "Together". ' + . 'Bundle product itself should be shipped instead.', + $this->validator->getMessages() + ) + ); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return MockObject + */ + private function getBundleOrderItemMock(): MockObject + { + return $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->addMethods(['getHasChildren']) + ->onlyMethods(['getItemId', 'isDummy', 'getProductType', 'getSku', 'getParentItem', 'getProduct']) + ->getMock(); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php index 9eb0e7aa8946c..2cf0c201f1203 100644 --- a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php @@ -14,12 +14,13 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; +use Magento\Ui\DataProvider\Modifier\PoolInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class BundleDataProviderTest extends TestCase { - const ALLOWED_TYPE = 'simple'; + private const ALLOWED_TYPE = 'simple'; /** * @var ObjectManager @@ -46,6 +47,11 @@ class BundleDataProviderTest extends TestCase */ protected $dataHelperMock; + /** + * @var PoolInterface|MockObject + */ + private $modifierPool; + /** * @return void */ @@ -53,6 +59,9 @@ protected function setUp(): void { $this->objectManager = new ObjectManager($this); + $this->modifierPool = $this->getMockBuilder(PoolInterface::class) + ->getMockForAbstractClass(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(Collection::class) @@ -97,6 +106,7 @@ protected function getModel() 'addFilterStrategies' => [], 'meta' => [], 'data' => [], + 'modifiersPool' => $this->modifierPool, ]); } @@ -128,6 +138,9 @@ public function testGetData() $this->collectionMock->expects($this->once()) ->method('getSize') ->willReturn(count($items)); + $this->modifierPool->expects($this->once()) + ->method('getModifiersInstances') + ->willReturn([]); $this->assertEquals($expectedData, $this->getModel()->getData()); } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php index 5f1ffc3c26823..827082dc77445 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider; use Magento\Bundle\Helper\Data; +use Magento\Framework\App\ObjectManager; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; +use Magento\Ui\DataProvider\Modifier\PoolInterface; class BundleDataProvider extends ProductDataProvider { @@ -16,6 +19,11 @@ class BundleDataProvider extends ProductDataProvider */ protected $dataHelper; + /** + * @var PoolInterface + */ + private $modifiersPool; + /** * Construct * @@ -24,10 +32,12 @@ class BundleDataProvider extends ProductDataProvider * @param string $requestFieldName * @param CollectionFactory $collectionFactory * @param Data $dataHelper - * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies - * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies * @param array $meta * @param array $data + * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies + * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies + * @param PoolInterface|null $modifiersPool + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -38,7 +48,8 @@ public function __construct( array $meta = [], array $data = [], array $addFieldStrategies = [], - array $addFilterStrategies = [] + array $addFilterStrategies = [], + PoolInterface $modifiersPool = null ) { parent::__construct( $name, @@ -52,6 +63,7 @@ public function __construct( ); $this->dataHelper = $dataHelper; + $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); } /** @@ -72,11 +84,34 @@ public function getData() ); $this->getCollection()->load(); } + $items = $this->getCollection()->toArray(); - return [ + $data = [ 'totalRecords' => $this->getCollection()->getSize(), 'items' => array_values($items), ]; + + /** @var ModifierInterface $modifier */ + foreach ($this->modifiersPool->getModifiersInstances() as $modifier) { + $data = $modifier->modifyData($data); + } + + return $data; + } + + /** + * @inheritdoc + */ + public function getMeta() + { + $meta = parent::getMeta(); + + /** @var ModifierInterface $modifier */ + foreach ($this->modifiersPool->getModifiersInstances() as $modifier) { + $meta = $modifier->modifyMeta($meta); + } + + return $meta; } } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php new file mode 100644 index 0000000000000..a2170aa30f8d3 --- /dev/null +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php @@ -0,0 +1,76 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; + +/** + * Affects Qty field for newly added selection + */ +class AddSelectionQtyTypeToProductsData implements ModifierInterface +{ + /** + * @var StockRegistryPreloader + */ + private StockRegistryPreloader $stockRegistryPreloader; + + /** + * Initializes dependencies + * + * @param StockRegistryPreloader $stockRegistryPreloader + */ + public function __construct(StockRegistryPreloader $stockRegistryPreloader) + { + $this->stockRegistryPreloader = $stockRegistryPreloader; + } + + /** + * Modify Meta + * + * @param array $meta + * @return array + */ + public function modifyMeta(array $meta) + { + return $meta; + } + + /** + * Modify Data - checks if new selection can have decimal quantity + * + * @param array $data + * @return array + * @throws NoSuchEntityException + */ + public function modifyData(array $data): array + { + $productIds = array_column($data['items'], 'entity_id'); + + $stockItems = []; + if ($productIds) { + $stockItems = $this->stockRegistryPreloader->preloadStockItems($productIds); + } + + $isQtyDecimals = []; + foreach ($stockItems as $stockItem) { + $isQtyDecimals[$stockItem->getProductId()] = $stockItem->getIsQtyDecimal(); + } + + foreach ($data['items'] as &$item) { + if (isset($isQtyDecimals[$item['entity_id']])) { + $item['selection_qty_is_integer'] = !$isQtyDecimals[$item['entity_id']]; + } + } + + return $data; + } +} diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index 4e2f17fa46d45..7b1c254eae6f9 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -403,6 +403,7 @@ protected function getBundleOptions() 'selection_price_type' => '', 'selection_price_value' => '', 'selection_qty' => '', + 'selection_qty_is_integer'=> 'selection_qty_is_integer', ], 'links' => [ 'insertData' => '${ $.provider }:${ $.dataProvider }', diff --git a/app/code/Magento/Bundle/etc/adminhtml/di.xml b/app/code/Magento/Bundle/etc/adminhtml/di.xml index f173bb26fcc3d..4f3069dee65b0 100644 --- a/app/code/Magento/Bundle/etc/adminhtml/di.xml +++ b/app/code/Magento/Bundle/etc/adminhtml/di.xml @@ -76,4 +76,21 @@ </argument> </arguments> </type> + <virtualType name="Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\ModifiersPool" type="Magento\Ui\DataProvider\Modifier\Pool"> + <arguments> + <argument name="modifiers" xsi:type="array"> + <item name="add_selection_qty_type_to_products_data" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\AddSelectionQtyTypeToProductsData</item> + <item name="sortOrder" xsi:type="number">200</item> + </item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Bundle\Ui\DataProvider\Product\BundleDataProvider"> + <arguments> + <argument name="modifiersPool" xsi:type="object"> + Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\ModifiersPool + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index c5c4a491234ed..7601224056bee 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -9,6 +9,7 @@ <preference for="Magento\Bundle\Api\ProductOptionTypeListInterface" type="Magento\Bundle\Model\OptionTypeList" /> <preference for="Magento\Bundle\Api\Data\OptionTypeInterface" type="Magento\Bundle\Model\Source\Option\Type" /> <preference for="Magento\Bundle\Api\ProductLinkManagementInterface" type="Magento\Bundle\Model\LinkManagement" /> + <preference for="Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface" type="Magento\Bundle\Model\LinkManagement" /> <preference for="Magento\Bundle\Api\Data\LinkInterface" type="Magento\Bundle\Model\Link" /> <preference for="Magento\Bundle\Api\ProductOptionRepositoryInterface" type="Magento\Bundle\Model\OptionRepository" /> <preference for="Magento\Bundle\Api\ProductOptionManagementInterface" type="Magento\Bundle\Model\OptionManagement" /> diff --git a/app/code/Magento/Bundle/etc/webapi_rest/di.xml b/app/code/Magento/Bundle/etc/webapi_rest/di.xml index 28a236d1fb359..29f2fd4494107 100644 --- a/app/code/Magento/Bundle/etc/webapi_rest/di.xml +++ b/app/code/Magento/Bundle/etc/webapi_rest/di.xml @@ -17,4 +17,9 @@ <plugin name="reindex_after_add_child_by_sku" type="Magento\Bundle\Plugin\Api\ProductLinkManagement\ReindexAfterAddChildBySkuPlugin"/> <plugin name="reindex_after_remove_child" type="Magento\Bundle\Plugin\Api\ProductLinkManagement\ReindexAfterRemoveChildPlugin"/> </type> + <type name="Magento\Sales\Model\Order\Shipment"> + <arguments> + <argument name="validator" xsi:type="object">Magento\Bundle\Model\Sales\Order\BundleOrderTypeValidator</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml index 26264cc2cc87f..a77a6654247da 100644 --- a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml @@ -6,6 +6,7 @@ ?> <?php +// @codingStandardsIgnoreFile $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; /** @var \Magento\Bundle\Pricing\Render\FinalPriceBox $block */ @@ -22,7 +23,7 @@ $regularPriceAttributes = [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, - 'skip_adjustments' => true + 'skip_adjustments' => false ]; $renderMinimalRegularPrice = $block->renderAmount($minimalRegularPrice, $regularPriceAttributes); ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 706b28049470e..5f3e219866ba6 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ use Magento\Bundle\ViewModel\ValidateQuantity; + +// phpcs:disable Generic.Files.LineLength.TooLong ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio */ ?> <?php $_option = $block->getOption(); ?> @@ -20,42 +22,45 @@ $viewModel = $block->getData('validateQuantityViewModel'); </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()) : ?> + <?php if ($block->showSingle()): ?> <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" - class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" - name="bundle_option[<?= (int)$_option->getId() ?>]" - value="<?= (int)$_selections[0]->getSelectionId() ?>" - id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" - checked="checked" + class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" + name="bundle_option[<?= (int)$_option->getId() ?>]" + value="<?= (int)$_selections[0]->getSelectionId() ?>" + id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" + checked="checked" /> - <?php else :?> - <?php if (!$_option->getRequired()) : ?> + <?php else: ?> + <?php if (!$_option->getRequired()): ?> <div class="field choice"> <input type="radio" class="radio product bundle option" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> + <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> value=""/> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> <span><?= $block->escapeHtml(__('None')) ?></span> </label> </div> <?php endif; ?> - <?php foreach ($_selections as $_selection) : ?> + <?php foreach ($_selections as $_selection): ?> <div class="field choice"> <input type="radio" class="radio product bundle option change-container-classname" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':true}"'; }?> + <?php if ($_option->getRequired()) { + echo 'data-validate="{\'validate-one-required-by-name\':true}"'; + } ?> name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> - <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> - value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> + <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + data-errors-message-box="#validation-message-box-radio"/> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> @@ -65,6 +70,7 @@ $viewModel = $block->getData('validateQuantityViewModel'); </div> <?php endforeach; ?> <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> + <div id="validation-message-box-radio"></div> <?php endif; ?> <div class="field qty qty-holder"> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> @@ -72,14 +78,14 @@ $viewModel = $block->getData('validateQuantityViewModel'); </label> <div class="control"> <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> - id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" - class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" - type="number" - min="0" - data-validate="<?= $block->escapeHtmlAttr($viewModel->getQuantityValidators()) ?>" - name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" + type="number" + min="0" + data-validate="<?= $block->escapeHtmlAttr($viewModel->getQuantityValidators()) ?>" + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> </div> </div> </div> diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php index 184f7177a995c..2d842b87faefc 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php @@ -7,12 +7,14 @@ namespace Magento\BundleGraphQl\Model\Resolver; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\BundleGraphQl\Model\Resolver\Links\Collection; +use Magento\BundleGraphQl\Model\Resolver\Links\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * @inheritdoc @@ -20,24 +22,28 @@ class BundleItemLinks implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $linkCollection; + private CollectionFactory $linkCollectionFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** - * @param Collection $linkCollection + * @param Collection $linkCollection Deprecated. Use $linkCollectionFactory instead * @param ValueFactory $valueFactory + * @param CollectionFactory|null $linkCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Collection $linkCollection, - ValueFactory $valueFactory + ValueFactory $valueFactory, + CollectionFactory $linkCollectionFactory = null ) { - $this->linkCollection = $linkCollection; + $this->linkCollectionFactory = $linkCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->valueFactory = $valueFactory; } @@ -49,12 +55,11 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['option_id']) || !isset($value['parent_id'])) { throw new LocalizedException(__('"option_id" and "parent_id" values should be specified')); } - - $this->linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); - $result = function () use ($value) { - return $this->linkCollection->getLinksForOptionId((int)$value['option_id']); + $linkCollection = $this->linkCollectionFactory->create(); + $linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); + $result = function () use ($value, $linkCollection) { + return $linkCollection->getLinksForOptionId((int)$value['option_id']); }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php index b67bd69ecf924..028772f5b2884 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php @@ -7,14 +7,16 @@ namespace Magento\BundleGraphQl\Model\Resolver; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type; use Magento\BundleGraphQl\Model\Resolver\Options\Collection; +use Magento\BundleGraphQl\Model\Resolver\Options\CollectionFactory; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * @inheritdoc @@ -22,39 +24,41 @@ class BundleItems implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $bundleOptionCollection; + private CollectionFactory $bundleOptionCollectionFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var MetadataPool */ - private $metadataPool; + private MetadataPool $metadataPool; /** - * @param Collection $bundleOptionCollection + * @param Collection $bundleOptionCollection Deprecated. Use $bundleOptionCollectionFactory * @param ValueFactory $valueFactory * @param MetadataPool $metadataPool + * @param CollectionFactory|null $bundleOptionCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Collection $bundleOptionCollection, ValueFactory $valueFactory, - MetadataPool $metadataPool + MetadataPool $metadataPool, + CollectionFactory $bundleOptionCollectionFactory = null ) { - $this->bundleOptionCollection = $bundleOptionCollection; + $this->bundleOptionCollectionFactory = $bundleOptionCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->valueFactory = $valueFactory; $this->metadataPool = $metadataPool; } /** - * Fetch and format bundle option items. - * - * {@inheritDoc} + * @inheritDoc */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { @@ -68,17 +72,15 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value }; return $this->valueFactory->create($result); } - - $this->bundleOptionCollection->addParentFilterData( + $bundleOptionCollection = $this->bundleOptionCollectionFactory->create(); + $bundleOptionCollection->addParentFilterData( (int)$value[$linkField], (int)$value['entity_id'], $value[ProductInterface::SKU] ); - - $result = function () use ($value, $linkField) { - return $this->bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]); + $result = function () use ($value, $linkField, $bundleOptionCollection) { + return $bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]); }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php index 660e65dc36f64..9a4e5b94c40a8 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php @@ -15,12 +15,13 @@ use Magento\Framework\Exception\RuntimeException; use Magento\Framework\GraphQl\Query\EnumLookup; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Zend_Db_Select_Exception; /** * Collection to fetch link data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -156,4 +157,14 @@ private function fetch() : array return $this->links; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->links = []; + $this->optionIds = []; + $this->parentIds = []; + } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index 2fa0ce6def9d3..5f1fe2c580a72 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -11,12 +11,13 @@ use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** * Collection to fetch bundle option data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * Option type name @@ -145,4 +146,13 @@ private function fetch() : array return $this->optionMap; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionMap = []; + $this->skuMap = []; + } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php index dfdf4e904a475..8da272dce33fb 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php @@ -7,12 +7,14 @@ namespace Magento\BundleGraphQl\Model\Resolver\Options; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Bundle product option label resolver @@ -22,21 +24,27 @@ class Label implements ResolverInterface /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** - * @var ProductDataProvider + * @var ProductDataProviderFactory */ - private $product; + private ProductDataProviderFactory $productFactory; /** * @param ValueFactory $valueFactory - * @param ProductDataProvider $product + * @param ProductDataProvider $product Deprecated. Use $productFactory + * @param ProductDataProviderFactory|null $productFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(ValueFactory $valueFactory, ProductDataProvider $product) - { + public function __construct( + ValueFactory $valueFactory, + ProductDataProvider $product, + ProductDataProviderFactory $productFactory = null + ) { $this->valueFactory = $valueFactory; - $this->product = $product; + $this->productFactory = $productFactory + ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class); } /** @@ -52,17 +60,15 @@ public function resolve( if (!isset($value['sku'])) { throw new LocalizedException(__('"sku" value should be specified')); } - - $this->product->addProductSku($value['sku']); - $this->product->addEavAttributes(['name']); - - $result = function () use ($value, $context) { - $productData = $this->product->getProductBySku($value['sku'], $context); + $product = $this->productFactory->create(); + $product->addProductSku($value['sku']); + $product->addEavAttributes(['name']); + $result = function () use ($value, $context, $product) { + $productData = $product->getProductBySku($value['sku'], $context); /** @var \Magento\Catalog\Model\Product $productModel */ $productModel = isset($productData['model']) ? $productData['model'] : null; return $productModel ? $productModel->getName() : null; }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index c6a6ba0eb05e5..2917a23d1005b 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -14,6 +14,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\ImportExport\Model\Import; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -24,7 +25,8 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType +class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType implements + ResetAfterRequestInterface { /** * Delimiter before product option value. @@ -783,4 +785,15 @@ private function getStoreIdByCode(string $storeCode): int return $this->storeCodeToId[$storeCode]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_cachedOptions = []; + $this->_cachedSkus = []; + $this->_cachedOptionSelectQuery = []; + $this->_cachedSkuToProducts = []; + } } diff --git a/app/code/Magento/CacheInvalidate/README.md b/app/code/Magento/CacheInvalidate/README.md index 6cca6ffec03e4..f12b0435e71bc 100644 --- a/app/code/Magento/CacheInvalidate/README.md +++ b/app/code/Magento/CacheInvalidate/README.md @@ -1,2 +1,2 @@ The CacheInvalidate module is used to invalidate the Varnish cache if it is configured. -It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL. \ No newline at end of file +It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL. diff --git a/app/code/Magento/Captcha/README.md b/app/code/Magento/Captcha/README.md index 35979fb2b4892..d4119e03e1d9f 100644 --- a/app/code/Magento/Captcha/README.md +++ b/app/code/Magento/Captcha/README.md @@ -1 +1 @@ -The Captcha module allows applying Turing test in the process of user authentication or similar tasks. \ No newline at end of file +The Captcha module allows applying Turing test in the process of user authentication or similar tasks. diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml index 132d5628b400d..6ebb0fda03089 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-93941"/> <group value="captcha"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!--Login as admin--> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml index 3a55535e33ae0..d89b80e76ab64 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml @@ -57,7 +57,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="AssertCaptchaVisibleOnSecondCheckoutStepActionGroup" stepKey="assertCaptchaIsVisible"/> <waitForPageLoad stepKey="waitForSpinner"/> <actionGroup ref="StorefrontFillCaptchaFieldOnCheckoutActionGroup" stepKey="placeOrderWithIncorrectValue"> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml index 912e637dc534e..4ab4ec7f055f9 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml @@ -21,6 +21,7 @@ <group value="storefront_captcha_enabled"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">20</field> @@ -62,6 +63,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Reindex and flush cache --> diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md index 54db9114a2a0e..aa68470a496bc 100644 --- a/app/code/Magento/CardinalCommerce/README.md +++ b/app/code/Magento/CardinalCommerce/README.md @@ -1 +1 @@ -The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. \ No newline at end of file +The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. diff --git a/app/code/Magento/Catalog/Block/Breadcrumbs.php b/app/code/Magento/Catalog/Block/Breadcrumbs.php index 674c99001b01a..558b833f0794a 100644 --- a/app/code/Magento/Catalog/Block/Breadcrumbs.php +++ b/app/code/Magento/Catalog/Block/Breadcrumbs.php @@ -16,8 +16,6 @@ class Breadcrumbs extends \Magento\Framework\View\Element\Template { /** - * Catalog data - * * @var Data */ protected $_catalogData = null; @@ -66,15 +64,11 @@ protected function _prepareLayout() ] ); - $title = []; $path = $this->_catalogData->getBreadcrumbPath(); foreach ($path as $name => $breadcrumb) { $breadcrumbsBlock->addCrumb($name, $breadcrumb); - $title[] = $breadcrumb['label']; } - - $this->pageConfig->getTitle()->set(join($this->getTitleSeparator(), array_reverse($title))); } return parent::_prepareLayout(); } diff --git a/app/code/Magento/Catalog/Block/Category/View.php b/app/code/Magento/Catalog/Block/Category/View.php index da0211ad20552..a91f33ba74340 100644 --- a/app/code/Magento/Catalog/Block/Category/View.php +++ b/app/code/Magento/Catalog/Block/Category/View.php @@ -6,23 +6,18 @@ namespace Magento\Catalog\Block\Category; /** - * Class View + * Category View Block class * @api - * @package Magento\Catalog\Block\Category * @since 100.0.2 */ class View extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** - * Catalog layer - * * @var \Magento\Catalog\Model\Layer */ protected $_catalogLayer; @@ -32,40 +27,56 @@ class View extends \Magento\Framework\View\Element\Template implements \Magento\ */ protected $_categoryHelper; + /** + * @var \Magento\Catalog\Helper\Data|null + */ + private $catalogData; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver * @param \Magento\Framework\Registry $registry * @param \Magento\Catalog\Helper\Category $categoryHelper * @param array $data + * @param \Magento\Catalog\Helper\Data|null $catalogData */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Catalog\Model\Layer\Resolver $layerResolver, \Magento\Framework\Registry $registry, \Magento\Catalog\Helper\Category $categoryHelper, - array $data = [] + array $data = [], + \Magento\Catalog\Helper\Data $catalogData = null ) { $this->_categoryHelper = $categoryHelper; $this->_catalogLayer = $layerResolver->get(); $this->_coreRegistry = $registry; + $this->catalogData = $catalogData ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\Helper\Data::class); parent::__construct($context, $data); } /** + * @inheritdoc * @return $this */ protected function _prepareLayout() { parent::_prepareLayout(); - $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class); + $block = $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class); $category = $this->getCurrentCategory(); if ($category) { $title = $category->getMetaTitle(); if ($title) { $this->pageConfig->getTitle()->set($title); + } else { + $title = []; + foreach ($this->catalogData->getBreadcrumbPath() as $breadcrumb) { + $title[] = $breadcrumb['label']; + } + $this->pageConfig->getTitle()->set(join($block->getTitleSeparator(), array_reverse($title))); } $description = $category->getMetaDescription(); if ($description) { @@ -93,6 +104,8 @@ protected function _prepareLayout() } /** + * Return Product list html + * * @return string */ public function getProductListHtml() @@ -114,6 +127,8 @@ public function getCurrentCategory() } /** + * Return CMS block html + * * @return mixed */ public function getCmsBlockHtml() @@ -131,6 +146,7 @@ public function getCmsBlockHtml() /** * Check if category display mode is "Products Only" + * * @return bool */ public function isProductMode() @@ -140,6 +156,7 @@ public function isProductMode() /** * Check if category display mode is "Static Block and Products" + * * @return bool */ public function isMixedMode() @@ -149,6 +166,7 @@ public function isMixedMode() /** * Check if category display mode is "Static Block Only" + * * For anchor category with applied filter Static Block Only mode not allowed * * @return bool diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 4491ad1ee99b5..95b7a9bfe5204 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -448,9 +448,9 @@ private function getLinkResolver() } /** - * Remove ids of non selected websites from $websiteIds array and return filtered data + * Remove ids of non-selected websites from $websiteIds array and return filtered data * - * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values + * $websiteIds parameter expects array with website ids as keys and id (selected) or 0 (non-selected) as values * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on * * @param array $websiteIds @@ -461,7 +461,8 @@ private function filterWebsiteIds($websiteIds) if (!$this->storeManager->isSingleStoreMode()) { $websiteIds = array_filter((array) $websiteIds); } else { - $websiteIds[$this->storeManager->getWebsite(true)->getId()] = 1; + $websiteId = $this->storeManager->getWebsite(true)->getId(); + $websiteIds[$websiteId] = $websiteId; } return $websiteIds; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php index 0b1ef98c386c4..ea14dbc1ce627 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php @@ -4,18 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\RegexValidator; class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var Initialization\StockDataFilter * @deprecated 101.0.0 + * @see Initialization\StockDataFilter */ protected $stockFilter; @@ -30,23 +33,32 @@ class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements protected $resultForwardFactory; /** - * @param Action\Context $context + * @var RegexValidator + */ + private RegexValidator $regexValidator; + + /** + * @param Context $context * @param Builder $productBuilder * @param Initialization\StockDataFilter $stockFilter * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param RegexValidator|null $regexValidator */ public function __construct( \Magento\Backend\App\Action\Context $context, Product\Builder $productBuilder, Initialization\StockDataFilter $stockFilter, \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + RegexValidator $regexValidator = null ) { $this->stockFilter = $stockFilter; parent::__construct($context, $productBuilder); $this->resultPageFactory = $resultPageFactory; $this->resultForwardFactory = $resultForwardFactory; + $this->regexValidator = $regexValidator + ?: ObjectManager::getInstance()->get(RegexValidator::class); } /** @@ -56,6 +68,11 @@ public function __construct( */ public function execute() { + $typeId = $this->getRequest()->getParam('type'); + if (!$this->regexValidator->validateParamRegex($typeId)) { + return $this->resultForwardFactory->create()->forward('noroute'); + } + if (!$this->getRequest()->getParam('set')) { return $this->resultForwardFactory->create()->forward('noroute'); } diff --git a/app/code/Magento/Catalog/Helper/Category.php b/app/code/Magento/Catalog/Helper/Category.php index ae42acf4b196e..c09ba2df1aac0 100644 --- a/app/code/Magento/Catalog/Helper/Category.php +++ b/app/code/Magento/Catalog/Helper/Category.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Category as ModelCategory; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -16,11 +17,11 @@ * * @SuppressWarnings(PHPMD.LongVariable) */ -class Category extends AbstractHelper +class Category extends AbstractHelper implements ResetAfterRequestInterface { - const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag'; + public const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag'; - const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id'; + public const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id'; /** * Store categories cache @@ -30,14 +31,14 @@ class Category extends AbstractHelper protected $_storeCategories = []; /** - * Store manager + * Store manager instance * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** - * Category factory + * Category factory instance * * @var \Magento\Catalog\Model\CategoryFactory */ @@ -176,4 +177,12 @@ public function canUseCanonicalTag($store = null) $store ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_storeCategories = []; + } } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 7082fa4747fdc..4e85853cd33bc 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; use Magento\Framework\Api\ExtensibleDataObjectConverter; @@ -16,6 +17,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -23,7 +25,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInterface +class CategoryRepository implements CategoryRepositoryInterface, ResetAfterRequestInterface { /** * @var Category[] @@ -231,6 +233,7 @@ protected function validateCategory(Category $category) * @return ExtensibleDataObjectConverter * * @deprecated 101.0.0 + * @see we don't recommend this approach anymore */ private function getExtensibleDataObjectConverter() { @@ -254,4 +257,12 @@ private function getMetadataPool() } return $this->metadataPool; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->instances = []; + } } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index c6feb049e1a10..f38597c67f2ab 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -15,12 +15,13 @@ use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** * Add data to category entity and populate with default values */ -class PopulateWithValues +class PopulateWithValues implements ResetAfterRequestInterface { /** * @var ScopeOverriddenValue @@ -150,4 +151,12 @@ private function getAttributes(): array return $this->attributes; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = []; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 49d8336dddb21..4119335d37b41 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -13,6 +13,7 @@ use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; @@ -24,8 +25,9 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ -abstract class AbstractAction +abstract class AbstractAction implements ResetAfterRequestInterface { /** * Chunk size @@ -44,7 +46,8 @@ abstract class AbstractAction /** * Suffix for table to show it is temporary - * @deprecated see getIndexTable + * @deprecated + * @see getIndexTable */ public const TEMPORARY_TABLE_SUFFIX = '_tmp'; @@ -156,6 +159,20 @@ public function __construct( $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->nonAnchorSelects = []; + $this->anchorSelects = []; + $this->productsSelects = []; + $this->categoryPath = []; + $this->useTempTable = true; + $this->tempTreeIndexTableName = null; + $this->currentStore = null; + } + /** * Run full reindex * diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php index 50700e672237e..fc06f6228d043 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; @@ -17,13 +18,22 @@ class Website */ private $tableMaintainer; + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param TableMaintainer $tableMaintainer + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut */ public function __construct( - TableMaintainer $tableMaintainer + TableMaintainer $tableMaintainer, + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { $this->tableMaintainer = $tableMaintainer; + $this->pillPut = $pillPut ?: ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); } /** @@ -35,12 +45,14 @@ public function __construct( * * @return AbstractDb * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Exception */ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website) { foreach ($website->getStoreIds() as $storeId) { $this->tableMaintainer->dropTablesForStore((int)$storeId); } + $this->pillPut->put(); return $objectResource; } } diff --git a/app/code/Magento/Catalog/Model/Layer.php b/app/code/Magento/Catalog/Model/Layer.php index fb94e82f0007c..65f2db475e02e 100644 --- a/app/code/Magento/Catalog/Model/Layer.php +++ b/app/code/Magento/Catalog/Model/Layer.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog view layer model @@ -17,7 +18,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Layer extends \Magento\Framework\DataObject +class Layer extends \Magento\Framework\DataObject implements ResetAfterRequestInterface { /** * Product collections array @@ -41,29 +42,21 @@ class Layer extends \Magento\Framework\DataObject protected $registry = null; /** - * Store manager - * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** - * Catalog product - * * @var \Magento\Catalog\Model\ResourceModel\Product */ protected $_catalogProduct; /** - * Attribute collection factory - * * @var AttributeCollectionFactory */ protected $_attributeCollectionFactory; /** - * Layer state factory - * * @var \Magento\Catalog\Model\Layer\StateFactory */ protected $_layerStateFactory; @@ -187,6 +180,7 @@ public function apply() /** * Retrieve current category model + * * If no category found in registry, the root will be taken * * @return \Magento\Catalog\Model\Category @@ -266,4 +260,12 @@ public function getState() return $state; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_productCollections = []; + } } diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php index 86a7d1fb61938..08d0441e919f2 100644 --- a/app/code/Magento/Catalog/Model/Layer/FilterList.php +++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php @@ -9,16 +9,17 @@ use Magento\Catalog\Model\Config\LayerCategoryConfig; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Layer navigation filters */ -class FilterList +class FilterList implements ResetAfterRequestInterface { - const CATEGORY_FILTER = 'category'; - const ATTRIBUTE_FILTER = 'attribute'; - const PRICE_FILTER = 'price'; - const DECIMAL_FILTER = 'decimal'; + public const CATEGORY_FILTER = 'category'; + public const ATTRIBUTE_FILTER = 'attribute'; + public const PRICE_FILTER = 'price'; + public const DECIMAL_FILTER = 'decimal'; /** * Filter factory @@ -131,4 +132,12 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\ return $filterClassName; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->filters = []; + } } diff --git a/app/code/Magento/Catalog/Model/Layer/Resolver.php b/app/code/Magento/Catalog/Model/Layer/Resolver.php index a4224aeafe7e0..b6ca16f1ac029 100644 --- a/app/code/Magento/Catalog/Model/Layer/Resolver.php +++ b/app/code/Magento/Catalog/Model/Layer/Resolver.php @@ -9,15 +9,17 @@ namespace Magento\Catalog\Model\Layer; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Layer Resolver * * @api */ -class Resolver +class Resolver implements ResetAfterRequestInterface { - const CATALOG_LAYER_CATEGORY = 'category'; - const CATALOG_LAYER_SEARCH = 'search'; + public const CATALOG_LAYER_CATEGORY = 'category'; + public const CATALOG_LAYER_SEARCH = 'search'; /** * Catalog view layer models list @@ -79,4 +81,12 @@ public function get() } return $this->layer; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->layer = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 0191c0239fc20..910d65a028382 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -16,6 +16,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\SaleableInterface; /** @@ -43,7 +44,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements IdentityInterface, SaleableInterface, - ProductInterface + ProductInterface, + ResetAfterRequestInterface { /** * @var ProductLinkRepositoryInterface @@ -55,22 +57,22 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements * Entity code. * Can be used as part of method name for entity processing */ - const ENTITY = 'catalog_product'; + public const ENTITY = 'catalog_product'; /** * Product cache tag */ - const CACHE_TAG = 'cat_p'; + public const CACHE_TAG = 'cat_p'; /** * Category product relation cache tag */ - const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p'; + public const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p'; /** * Product Store Id */ - const STORE_ID = 'store_id'; + public const STORE_ID = 'store_id'; /** * @var string|bool @@ -170,8 +172,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements protected $_calculatePrice = true; /** - * Catalog product - * * @var \Magento\Catalog\Helper\Product */ protected $_catalogProduct = null; @@ -187,43 +187,31 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements protected $_collectionFactory; /** - * Catalog product type - * * @var Product\Type */ protected $_catalogProductType; /** - * Catalog product media config - * * @var Product\Media\Config */ protected $_catalogProductMediaConfig; /** - * Catalog product status - * * @var Status */ protected $_catalogProductStatus; /** - * Catalog product visibility - * * @var Product\Visibility */ protected $_catalogProductVisibility; /** - * Stock item factory - * * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory */ protected $_stockItemFactory; /** - * Item option factory - * * @var \Magento\Catalog\Model\Product\Configuration\Item\OptionFactory */ protected $_itemOptionFactory; @@ -279,27 +267,28 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface - * @deprecated 102.0.6 Not used anymore due to performance issue (loaded all product attributes) + * @deprecated 102.0.6 + * @see Not used anymore due to performance issue (loaded all product attributes) */ protected $metadataService; /** - * @param \Magento\Catalog\Model\ProductLink\CollectionProvider + * @var \Magento\Catalog\Model\ProductLink\CollectionProvider */ protected $entityCollectionProvider; /** - * @param \Magento\Catalog\Model\Product\LinkTypeProvider + * @var \Magento\Catalog\Model\Product\LinkTypeProvider */ protected $linkProvider; /** - * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory + * @var \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory */ protected $productLinkFactory; /** - * @param \Magento\Catalog\Api\Data\ProductLinkExtensionFactory + * @var \Magento\Catalog\Api\Data\ProductLinkExtensionFactory */ protected $productLinkExtensionFactory; @@ -494,7 +483,8 @@ protected function _construct() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Product - * @deprecated 102.0.6 because resource models should be used directly + * @deprecated 102.0.6 + * @see \Magento\Catalog\Model\ResourceModel\Product * @since 102.0.6 */ protected function _getResource() @@ -643,6 +633,7 @@ public function getUpdatedAt() * @param bool $calculate * @return void * @deprecated 102.0.4 + * @see we don't recommend this approach anymore */ public function setPriceCalculation($calculate = true) { @@ -2841,4 +2832,15 @@ public function setStockData($stockData) $this->setData('stock_data', $stockData); return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_customOptions = []; + $this->_errors = []; + $this->_canAffectOptions = []; + $this->_productIdCached = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php index 68aeabfc70d34..4623c095e6d8c 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php @@ -10,13 +10,14 @@ use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product\Attribute\Backend\Price; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog product abstract group price backend attribute model * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class AbstractGroupPrice extends Price +abstract class AbstractGroupPrice extends Price implements ResetAfterRequestInterface { /** * @var \Magento\Framework\EntityManager\MetadataPool @@ -26,7 +27,7 @@ abstract class AbstractGroupPrice extends Price /** * Website currency codes and rates * - * @var array + * @var array|null */ protected $_rates; @@ -39,8 +40,6 @@ abstract class AbstractGroupPrice extends Price abstract protected function _getDuplicateErrorMessage(); /** - * Catalog product type - * * @var \Magento\Catalog\Model\Product\Type */ protected $_catalogProductType; @@ -82,6 +81,14 @@ public function __construct( ); } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->_rates = null; + } + /** * Retrieve websites currency rates and base currency codes * diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php index c0a13aa8b934a..020176738160e 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php @@ -11,50 +11,62 @@ */ namespace Magento\Catalog\Model\Product\Attribute\Source; +use Magento\Directory\Model\CountryFactory; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Framework\App\Cache\Type\Config; use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; class Countryofmanufacture extends AbstractSource implements OptionSourceInterface { /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $_configCacheType; /** - * Store manager - * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * Country factory - * - * @var \Magento\Directory\Model\CountryFactory + * @var CountryFactory */ protected $_countryFactory; /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var SerializerInterface */ private $serializer; + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * Construct * - * @param \Magento\Directory\Model\CountryFactory $countryFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\App\Cache\Type\Config $configCacheType + * @param CountryFactory $countryFactory + * @param StoreManagerInterface $storeManager + * @param Config $configCacheType + * @param ResolverInterface $localeResolver + * @param SerializerInterface $serializer */ public function __construct( - \Magento\Directory\Model\CountryFactory $countryFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\App\Cache\Type\Config $configCacheType + CountryFactory $countryFactory, + StoreManagerInterface $storeManager, + Config $configCacheType, + ResolverInterface $localeResolver, + SerializerInterface $serializer ) { $this->_countryFactory = $countryFactory; $this->_storeManager = $storeManager; $this->_configCacheType = $configCacheType; + $this->localeResolver = $localeResolver; + $this->serializer = $serializer; } /** @@ -64,32 +76,20 @@ public function __construct( */ public function getAllOptions() { - $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $this->_storeManager->getStore()->getCode(); + $storeCode = $this->_storeManager->getStore()->getCode(); + $locale = $this->localeResolver->getLocale(); + + $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $storeCode . '_LOCALE_' . $locale; if ($cache = $this->_configCacheType->load($cacheKey)) { - $options = $this->getSerializer()->unserialize($cache); + $options = $this->serializer->unserialize($cache); } else { /** @var \Magento\Directory\Model\Country $country */ $country = $this->_countryFactory->create(); /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $collection */ $collection = $country->getResourceCollection(); $options = $collection->load()->toOptionArray(); - $this->_configCacheType->save($this->getSerializer()->serialize($options), $cacheKey); + $this->_configCacheType->save($this->serializer->serialize($options), $cacheKey); } return $options; } - - /** - * Get serializer - * - * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 102.0.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\SerializerInterface::class); - } - return $this->serializer; - } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 4034c75f7373c..85a69a9e69be0 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\AwsS3\Driver\AwsS3; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -287,10 +288,18 @@ private function getImageContent($product, $entry): ImageContentInterface $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $path = $mediaDirectory->getAbsolutePath($product->getMediaConfig()->getMediaPath($entry->getFile())); $fileName = $this->file->getPathInfo($path)['basename']; - $imageFileContent = $mediaDirectory->getDriver()->fileGetContents($path); + $fileDriver = $mediaDirectory->getDriver(); + $imageFileContent = $fileDriver->fileGetContents($path); + + if ($fileDriver instanceof AwsS3) { + $remoteMediaMimeType = $fileDriver->getMetadata($path); + $mediaMimeType = $remoteMediaMimeType['mimetype']; + } else { + $mediaMimeType = $this->mime->getMimeType($path); + } return $this->imageContentInterface->create() ->setName($fileName) ->setBase64EncodedData(base64_encode($imageFileContent)) - ->setType($this->mime->getMimeType($path)); + ->setType($mediaMimeType); } } diff --git a/app/code/Magento/Catalog/Model/Product/Image/Cache.php b/app/code/Magento/Catalog/Model/Product/Image/Cache.php index c5e5e0ecac4c0..0105a224b2c0d 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/Cache.php +++ b/app/code/Magento/Catalog/Model/Product/Image/Cache.php @@ -7,11 +7,12 @@ use Magento\Catalog\Helper\Image as ImageHelper; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection; use Magento\Framework\App\Area; use Magento\Framework\View\ConfigInterface; -class Cache +class Cache implements ResetAfterRequestInterface { /** * @var ConfigInterface @@ -66,6 +67,7 @@ protected function getData() ]); $images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE); foreach ($images as $imageId => $imageData) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->data[$theme->getCode() . $imageId] = array_merge(['id' => $imageId], $imageData); } } @@ -127,4 +129,12 @@ protected function processImageData(Product $product, array $imageData, $file) return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 2297f39829aa3..99c2513f5187a 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; @@ -16,7 +17,7 @@ * @api * @since 100.0.2 */ -class Config implements ConfigInterface +class Config implements ConfigInterface, ResetAfterRequestInterface { /** * @var StoreManagerInterface @@ -29,7 +30,7 @@ class Config implements ConfigInterface private $attributeHelper; /** - * @var string[] + * @var string[]|null */ private $mediaAttributeCodes; @@ -199,4 +200,12 @@ private function getAttributeHelper() } return $this->attributeHelper; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->mediaAttributeCodes = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 225f1bb3d10e3..6a894278a0494 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Option\Value; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog product option default type @@ -23,7 +24,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class DefaultType extends \Magento\Framework\DataObject +class DefaultType extends \Magento\Framework\DataObject implements ResetAfterRequestInterface { /** * Option Instance @@ -426,4 +427,12 @@ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) return $price; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_productOptions = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php new file mode 100644 index 0000000000000..609d02e33757a --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Option\Type\File; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Request\Http as Request; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\File\Size; +use Magento\Framework\Filesystem; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Math\Random; +use Magento\Framework\Validator\File\IsImage; + +/** + * Request Aware Validator to replace use of $_SERVER super global. + */ +class RequestAwareValidatorFile extends ValidatorFile +{ + /** + * @var Request $request + */ + private Request $request; + + /** + * Constructor method + * + * @param ScopeConfigInterface $scopeConfig + * @param Filesystem $filesystem + * @param Size $fileSize + * @param FileTransferFactory $httpFactory + * @param IsImage $isImageValidator + * @param Random|null $random + * @param Request|null $request + * @throws FileSystemException + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Filesystem $filesystem, + Size $fileSize, + FileTransferFactory $httpFactory, + IsImage $isImageValidator, + Random $random = null, + Request $request = null + ) { + $this->request = $request ?: ObjectManager::getInstance()->get(Request::class); + parent::__construct( + $scopeConfig, + $filesystem, + $fileSize, + $httpFactory, + $isImageValidator, + $random + ); + } + + /** + * @inheritDoc + */ + protected function validateContentLength(): bool + { + return isset($this->request->getServer()['CONTENT_LENGTH']) + && $this->request->getServer()['CONTENT_LENGTH'] > $this->fileSize->getMaxFileSize(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php index f23ce0faf708c..6bbb0951ec192 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php @@ -8,8 +8,9 @@ use Magento\Catalog\Api\Data\TierPriceInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -class TierPriceFactory +class TierPriceFactory implements ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\Data\TierPriceInterfaceFactory @@ -168,4 +169,12 @@ private function retrieveGroupValue($code) return $this->customerGroupsByCode[$code]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupsByCode = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php index 45ba1de85260d..872a3e31eb573 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php @@ -14,6 +14,7 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\WebsiteRepositoryInterface; /** @@ -21,7 +22,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TierPriceValidator +class TierPriceValidator implements ResetAfterRequestInterface { /** * @var ProductIdLocatorInterface @@ -475,10 +476,19 @@ private function retrieveGroupValue(string $code) $item = array_shift($items); if (!$item) { + $this->customerGroupsByCode[$code] = false; return false; } - $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId(); + $itemCode = $item->getCode(); + $itemId = $item->getId(); + + if (strtolower($itemCode) !== $code) { + $this->customerGroupsByCode[$code] = false; + return false; + } + + $this->customerGroupsByCode[strtolower($itemCode)] = $itemId; } return $this->customerGroupsByCode[$code]; @@ -499,4 +509,12 @@ private function compareWebsiteValue(TierPriceInterface $price, TierPriceInterfa ) && $price->getWebsiteId() != $tierPrice->getWebsiteId(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupsByCode = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index fb25b6703b730..eee62527094f6 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; @@ -23,7 +24,7 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ -class Price +class Price implements ResetAfterRequestInterface { /** * Product price cache tag @@ -657,4 +658,12 @@ public function isTierPriceFixed() { return true; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$attributeCache = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php index c3a88a505c516..d8af4a9656304 100644 --- a/app/code/Magento/Catalog/Model/ProductCategoryList.php +++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php @@ -8,13 +8,14 @@ use Magento\Framework\DB\Select; use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Provides info about product categories. */ -class ProductCategoryList +class ProductCategoryList implements ResetAfterRequestInterface { /** * @var array @@ -106,4 +107,12 @@ public function getCategorySelect($productId, $tableName) $productId ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->categoryIdList = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php index daf8790c419f9..eb493671e613c 100644 --- a/app/code/Magento/Catalog/Model/ProductIdLocator.php +++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php @@ -6,10 +6,12 @@ namespace Magento\Catalog\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Product ID locator provides all product IDs by SKUs. */ -class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface +class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface, ResetAfterRequestInterface { /** * Limit values for array IDs by SKU. @@ -126,4 +128,12 @@ private function truncateToLimit() $this->idsBySku = array_slice($this->idsBySku, $this->idsLimit * -1, null, true); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->idsBySku = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 5cf4ac1e64242..c586563759b54 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -31,6 +31,7 @@ use Magento\Framework\Exception\StateException; use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\EavAttributeInterface; @@ -40,8 +41,10 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ -class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface +class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface, ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface @@ -794,6 +797,7 @@ private function getMediaGalleryProcessor() /** * Retrieve collection processor * + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 102.0.0 * @return CollectionProcessorInterface */ @@ -952,4 +956,13 @@ private function joinPositionField( ); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->instances = []; + $this->instancesById = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php index 61f2c1838a1c2..1c2a315490f36 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php @@ -14,25 +14,24 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\FlagManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -/** - * Class WebsiteAttributesSynchronizer - * @package Magento\Catalog\Cron - */ -class WebsiteAttributesSynchronizer +class WebsiteAttributesSynchronizer implements ResetAfterRequestInterface { - const FLAG_SYNCHRONIZED = 0; - const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1; - const FLAG_REQUIRES_SYNCHRONIZATION = 2; - const FLAG_NAME = 'catalog_website_attribute_is_sync_required'; + public const FLAG_SYNCHRONIZED = 0; + public const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1; + public const FLAG_REQUIRES_SYNCHRONIZATION = 2; + public const FLAG_NAME = 'catalog_website_attribute_is_sync_required'; - const ATTRIBUTE_WEBSITE = 2; - const GLOBAL_STORE_VIEW_ID = 0; + public const ATTRIBUTE_WEBSITE = 2; + public const GLOBAL_STORE_VIEW_ID = 0; - const MASK_ATTRIBUTE_VALUE = '%d_%d_%d'; + public const MASK_ATTRIBUTE_VALUE = '%d_%d_%d'; /** * Map table names to metadata classes where link field might be found + * + * @var string[] */ private $tableMetaDataClass = [ 'catalog_category_entity_datetime' => CategoryInterface::class, @@ -101,7 +100,7 @@ class WebsiteAttributesSynchronizer * WebsiteAttributesSynchronizer constructor. * @param ResourceConnection $resourceConnection * @param FlagManager $flagManager - * @param Generator $batchQueryGenerator, + * @param Generator $batchQueryGenerator * @param MetadataPool $metadataPool */ public function __construct( @@ -119,6 +118,7 @@ public function __construct( /** * Synchronizes attribute values between different store views on website level + * * @return void * @throws \Exception */ @@ -141,15 +141,18 @@ public function synchronize() } /** + * Check if synchronization required + * * @return bool */ - public function isSynchronizationRequired() + public function isSynchronizationRequired(): bool { return self::FLAG_REQUIRES_SYNCHRONIZATION === $this->flagManager->getFlagData(self::FLAG_NAME); } /** * Puts a flag that synchronization is required + * * @return void */ public function scheduleSynchronization() @@ -159,6 +162,7 @@ public function scheduleSynchronization() /** * Marks flag as in progress in case if several crons enabled, so sync. won't be duplicated + * * @return void */ private function markSynchronizationInProgress() @@ -168,6 +172,7 @@ private function markSynchronizationInProgress() /** * Turn off synchronization flag + * * @return void */ private function markSynchronized() @@ -176,10 +181,12 @@ private function markSynchronized() } /** + * Perform table synchronization + * * @param string $tableName * @return void */ - private function synchronizeTable($tableName) + private function synchronizeTable(string $tableName): void { foreach ($this->fetchAttributeValues($tableName) as $attributeValueItems) { $this->processAttributeValues($attributeValueItems, $tableName); @@ -188,6 +195,7 @@ private function synchronizeTable($tableName) /** * Aligns website attribute values + * * @param array $attributeValueItems * @param string $tableName * @return void @@ -215,7 +223,7 @@ private function processAttributeValues(array $attributeValueItems, $tableName) * * @param string $tableName * @yield array - * @return void + * @return \Generator */ private function fetchAttributeValues($tableName) { @@ -257,6 +265,8 @@ private function fetchAttributeValues($tableName) } /** + * Retrieve grouped store views + * * @return array */ private function getGroupedStoreViews() @@ -286,6 +296,8 @@ private function getGroupedStoreViews() } /** + * Check if attribute value processed + * * @param array $attributeValue * @param string $tableName * @return bool @@ -304,6 +316,7 @@ private function isAttributeValueProcessed(array $attributeValue, $tableName) /** * Resets processed attribute values + * * @return void */ private function resetProcessedAttributeValues() @@ -312,6 +325,8 @@ private function resetProcessedAttributeValues() } /** + * Mark processed attribute value + * * @param array $attributeValue * @param string $tableName * @return void @@ -326,6 +341,8 @@ private function markAttributeValueProcessed(array $attributeValue, $tableName) } /** + * Retrieve attribute value key + * * @param int $entityId * @param int $attributeId * @param int $websiteId @@ -342,6 +359,8 @@ private function getAttributeValueKey($entityId, $attributeId, $websiteId) } /** + * Generate insertions for attribute value + * * @param array $attributeValue * @param string $tableName * @return array|null @@ -369,6 +388,8 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN } /** + * Insert attribute values into table + * * @param array $insertions * @param string $tableName * @return void @@ -376,9 +397,9 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN private function executeInsertions(array $insertions, $tableName) { $rawQuery = sprintf( - 'INSERT INTO + 'INSERT INTO %s(attribute_id, store_id, %s, `value`) - VALUES + VALUES %s ON duplicate KEY UPDATE `value` = VALUES(`value`)', $this->resourceConnection->getTableName($tableName), @@ -399,13 +420,9 @@ private function getPlaceholderValues(array $insertions) { $placeholderValues = []; foreach ($insertions as $insertion) { - $placeholderValues = array_merge( - $placeholderValues, - $insertion - ); + $placeholderValues[] = $insertion; } - - return $placeholderValues; + return array_merge(...$placeholderValues); } /** @@ -426,6 +443,8 @@ private function prepareInsertValuesStatement(array $insertions) } /** + * Retrieve table link field + * * @param string $tableName * @return string * @throws LocalizedException @@ -449,4 +468,14 @@ private function getTableLinkField($tableName) return $this->linkFields[$tableName]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->groupedStoreViews = []; + $this->processedAttributeValues = []; + $this->linkFields = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 35828fc8ec117..765065bb5fded 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -20,13 +20,14 @@ use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Resource model for category entity * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Category extends AbstractResource +class Category extends AbstractResource implements ResetAfterRequestInterface { /** * Category tree object @@ -1172,4 +1173,14 @@ public function getCategoryWithChildren(int $categoryId): array return $connection->fetchAll($select); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->entitiesWhereAttributesIs = []; + $this->_storeId = null; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 56fb7290b81a6..9df0a3a9b3831 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -137,6 +137,18 @@ protected function _construct() $this->_init(Category::class, \Magento\Catalog\Model\ResourceModel\Category::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_productTable = null; + $this->_productStoreId = null; + $this->_productWebsiteTable = null; + $this->_loadWithProductCount = false; + } + /** * Add Id filter * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index a121648b7acba..89dacf5361a69 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -76,6 +76,15 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_storeId = null; + } + /** * Retrieve Entity Primary Key * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index c950b49348dc3..1256ab1caa93b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -16,6 +16,7 @@ use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Product entity resource model @@ -23,9 +24,10 @@ * @api * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @since 100.0.2 */ -class Product extends AbstractResource +class Product extends AbstractResource implements ResetAfterRequestInterface { /** * Product to website linkage table @@ -844,4 +846,13 @@ protected function _afterDelete(DataObject $object) $this->mediaImageDeleteProcessor->execute($object); return parent::_afterDelete($object); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->availableCategoryIdsCache = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 79636c55c0f56..6756aac0786a9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -102,6 +102,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ protected $_productLimitationFilters; + /** + * @var ProductLimitationFactory + */ + private $productLimitationFactory; + /** * Category product count select * @@ -354,10 +359,10 @@ public function __construct( $this->_resourceHelper = $resourceHelper; $this->dateTime = $dateTime; $this->_groupManagement = $groupManagement; - $productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get( + $this->productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get( \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory::class ); - $this->_productLimitationFilters = $productLimitationFactory->create(); + $this->_productLimitationFilters = $this->productLimitationFactory->create(); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); parent::__construct( $entityFactory, @@ -387,6 +392,36 @@ public function __construct( ->get(Gallery::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_flatEnabled = []; + $this->_addUrlRewrite = false; + $this->_urlRewriteCategory = ''; + $this->_addFinalPrice = false; + $this->_allIdsCache = null; + $this->_addTaxPercents = false; + $this->_productLimitationFilters = $this->productLimitationFactory->create(); + $this->_productCountSelect = null; + $this->_isWebsiteFilter = false; + $this->_priceDataFieldFilters = []; + $this->_priceExpression = null; + $this->_additionalPriceExpression = null; + $this->_maxPrice = null; + $this->_minPrice = null; + $this->_priceStandardDeviation = null; + $this->_pricesCount = null; + $this->_catalogPreparePriceSelect = null; + $this->needToAddWebsiteNamesToResult = null; + $this->linkField = null; + $this->backend = null; + $this->emptyItem = null; + $this->_construct(); + } + /** * Get cloned Select after dispatching 'catalog_prepare_price_select' event * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php index 76f566a364769..7100f20ecd96f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -46,15 +46,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection protected $_comparableAttributes; /** - * Catalog product compare - * * @var \Magento\Catalog\Helper\Product\Compare */ protected $_catalogProductCompare = null; /** - * Catalog product compare item - * * @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item */ protected $_catalogProductCompareItem; @@ -150,6 +146,18 @@ protected function _construct() $this->_initTables(); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_customerId = 0; + $this->_visitorId = 0; + $this->listId = 0; + $this->_comparableAttributes = null; + } + /** * Set customer filter to collection * @@ -287,7 +295,6 @@ public function getProductsByListId(int $listId): array return $this->getConnection()->fetchCol($select); } - /** * Set list_id for customer compare item * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php index bca919e700364..cac549e0a17c9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php @@ -183,6 +183,21 @@ public function __construct( } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_product = null; + $this->_linkModel = null; + $this->_linkTypeId = null; + $this->_isStrongMode = null; + $this->_hasLinkFilter = false; + $this->productIds = null; + $this->linkField = null; + } + /** * Declare link model and initialize type attributes join * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php index 43762306b2b69..f7c02cc93bf97 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php @@ -10,16 +10,17 @@ * * @author Magento Core Team <core@magentocommerce.com> */ -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** * Stores configuration array @@ -727,4 +728,15 @@ private function getMetadataPool() } return $this->metadataPool; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_categoryAttributes = []; + $this->_productAttributes = []; + $this->_rootChildrenIds = []; + $this->_stores = null; + } } diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index a5e573caa381e..6945bf526cffb 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -6,9 +6,9 @@ namespace Magento\Catalog\Pricing\Price; -use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; /** * As Low As shows minimal value of Tier Prices @@ -36,18 +36,7 @@ public function __construct(CalculatorInterface $calculator) */ public function getValue(SaleableInterface $saleableItem) { - /** @var TierPrice $price */ - $price = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); - $tierPriceList = $price->getTierPriceList(); - - $tierPrices = []; - foreach ($tierPriceList as $tierPrice) { - /** @var AmountInterface $price */ - $price = $tierPrice['price']; - $tierPrices[] = $price->getValue(); - } - - return $tierPrices ? min($tierPrices) : null; + return $this->getAmount($saleableItem)?->getValue(); } /** @@ -58,10 +47,16 @@ public function getValue(SaleableInterface $saleableItem) */ public function getAmount(SaleableInterface $saleableItem) { - $value = $this->getValue($saleableItem); + $minPrice = null; + /** @var TierPrice $price */ + $tierPrice = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); + $tierPriceList = $tierPrice->getTierPriceList(); + + if (count($tierPriceList)) { + usort($tierPriceList, fn ($tier1, $tier2) => $tier1['price']->getValue() <=> $tier2['price']->getValue()); + $minPrice = array_shift($tierPriceList)['price']; + } - return $value === null - ? null - : $this->calculator->getAmount($value, $saleableItem, 'tax'); + return $minPrice; } } diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index 5fba207bdeb0c..6033e7deaeac9 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -6,16 +6,17 @@ namespace Magento\Catalog\Pricing\Render; -use Magento\Catalog\Pricing\Price; -use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; -use Magento\Msrp\Pricing\Price\MsrpPrice; use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; -use Magento\Framework\View\Element\Template\Context; -use Magento\Framework\Pricing\SaleableInterface; +use Magento\Catalog\Pricing\Price; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\Adjustment\CalculatorInterface; use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; use Magento\Framework\Pricing\Render\RendererPool; -use Magento\Framework\App\ObjectManager; -use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Msrp\Pricing\Price\MsrpPrice; /** * Class for final_price rendering @@ -140,7 +141,7 @@ public function renderAmountMinimal() 'display_label' => __('As low as'), 'price_id' => $id, 'include_container' => false, - 'skip_adjustments' => true + 'skip_adjustments' => false ] ); } @@ -183,7 +184,7 @@ public function showMinimalPrice() public function getCacheKey() { return parent::getCacheKey() - . ($this->getData('list_category_page') ? '-list-category-page': '') + . ($this->getData('list_category_page') ? '-list-category-page' : '') . ($this->getSaleableItem()->getCustomerGroupId() ?? ''); } diff --git a/app/code/Magento/Catalog/README.md b/app/code/Magento/Catalog/README.md index 0e43661ba8cae..ef95c1effe0a5 100644 --- a/app/code/Magento/Catalog/README.md +++ b/app/code/Magento/Catalog/README.md @@ -1,7 +1,9 @@ -#Magento_Catalog +# Magento_Catalog + Magento_Catalog module functionality is represented by the following sub-systems: - - Products Management. It includes CRUD operation of product, product media, product attributes, etc... - - Category Management. It includes CRUD operation of category, category attributes + +- Products Management. It includes CRUD operation of product, product media, product attributes, etc... +- Category Management. It includes CRUD operation of category, category attributes Catalog module provides mechanism for creating new product type in the system. Catalog module provides API filtering that allows to limit product selection with advanced filters. @@ -12,61 +14,61 @@ Catalog module provides API filtering that allows to limit product selection wit (https://developer.adobe.com/commerce/php/development/build/component-file-structure/). ## Observer + This module observes the following events: - `etc/events.xml` - `magento_catalog_api_data_productinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_productinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_productinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_productinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_productinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `magento_catalog_api_data_categoryinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_categoryinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_categoryinterface_save_after` event in - `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file. - `magento_catalog_api_data_categoryinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_categoryinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_categoryinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `magento_catalog_api_data_categorytreeinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_categorytreeinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_categorytreeinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_categorytreeinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_categorytreeinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `admin_system_config_changed_section_catalog` event in - `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file. - `catalog_product_save_before` event in - `Magento\Catalog\Observer\SetSpecialPriceStartDate` file. - `store_save_after` event in - `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file. - `catalog_product_save_commit_after` event in - `Magento\Catalog\Observer\ImageResizeAfterProductSave` file. - `catalog_category_prepare_save` event in - `Magento\Catalog\Observer\CategoryDesignAuthorization` file. - - `/etc/frontend/events.xml` - `customer_login` event in - `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file. - `customer_logout` event in - `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file. - - `/etc/adminhtml/events.xml` - `cms_wysiwyg_images_static_urls_allowed` event in - `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file. - `catalog_category_change_products` event in - `Magento\Catalog\Observer\CategoryProductIndexer` file. - `category_move` event in - `Magento\Catalog\Observer\FlushCategoryPagesCache` \ No newline at end of file + +- `etc/events.xml` + - `magento_catalog_api_data_productinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_productinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_productinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_productinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_productinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + - `magento_catalog_api_data_categoryinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_categoryinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_categoryinterface_save_after` event in + `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file. + - `magento_catalog_api_data_categoryinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_categoryinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_categoryinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + - `magento_catalog_api_data_categorytreeinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_categorytreeinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_categorytreeinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_categorytreeinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_categorytreeinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + `admin_system_config_changed_section_catalog` event in + `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file. + - `catalog_product_save_before` event in + `Magento\Catalog\Observer\SetSpecialPriceStartDate` file. + `store_save_after` event in + `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file. + - `catalog_product_save_commit_after` event in + `Magento\Catalog\Observer\ImageResizeAfterProductSave` file. + - `catalog_category_prepare_save` event in + `Magento\Catalog\Observer\CategoryDesignAuthorization` file. +- `/etc/frontend/events.xml` + - `customer_login` event in + `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file. + - `customer_logout` event in + `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file. +- `/etc/adminhtml/events.xml` + `cms_wysiwyg_images_static_urls_allowed` event in + `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file. + - `catalog_category_change_products` event in + `Magento\Catalog\Observer\CategoryProductIndexer` file. + - `category_move` event in + `Magento\Catalog\Observer\FlushCategoryPagesCache` diff --git a/app/code/Magento/Catalog/Test/Fixture/Attribute.php b/app/code/Magento/Catalog/Test/Fixture/Attribute.php index 1f68eb2b832d3..f4efdcf5038c8 100644 --- a/app/code/Magento/Catalog/Test/Fixture/Attribute.php +++ b/app/code/Magento/Catalog/Test/Fixture/Attribute.php @@ -11,8 +11,12 @@ use Magento\Catalog\Api\ProductAttributeManagementInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Attribute as ResourceModelAttribute; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Eav\Model\AttributeFactory; use Magento\Eav\Setup\EavSetup; use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\DataMerger; use Magento\TestFramework\Fixture\Api\ServiceFactory; use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; use Magento\TestFramework\Fixture\Data\ProcessorInterface; @@ -30,12 +34,12 @@ class Attribute implements RevertibleDataFixtureInterface 'is_filterable_in_grid' => true, 'position' => 0, 'apply_to' => [], - 'is_searchable' => '0', - 'is_visible_in_advanced_search' => '0', - 'is_comparable' => '0', - 'is_used_for_promo_rules' => '0', - 'is_visible_on_front' => '0', - 'used_in_product_listing' => '0', + 'is_searchable' => false, + 'is_visible_in_advanced_search' => false, + 'is_comparable' => false, + 'is_used_for_promo_rules' => false, + 'is_visible_on_front' => false, + 'used_in_product_listing' => false, 'is_visible' => true, 'scope' => 'store', 'attribute_code' => 'product_attribute%uniqid%', @@ -49,7 +53,6 @@ class Attribute implements RevertibleDataFixtureInterface 'backend_type' => 'varchar', 'is_unique' => '0', 'validation_rules' => [] - ]; private const DEFAULT_ATTRIBUTE_SET_DATA = [ @@ -78,29 +81,59 @@ class Attribute implements RevertibleDataFixtureInterface */ private $productAttributeManagement; + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + /** * @param ServiceFactory $serviceFactory * @param ProcessorInterface $dataProcessor * @param EavSetup $eavSetup + * @param ProductAttributeManagementInterface $productAttributeManagement + * @param AttributeFactory $attributeFactory + * @param DataMerger $dataMerger + * @param ResourceModelAttribute $resourceModelAttribute */ public function __construct( ServiceFactory $serviceFactory, ProcessorInterface $dataProcessor, EavSetup $eavSetup, - ProductAttributeManagementInterface $productAttributeManagement + ProductAttributeManagementInterface $productAttributeManagement, + AttributeFactory $attributeFactory, + DataMerger $dataMerger, + ResourceModelAttribute $resourceModelAttribute ) { $this->serviceFactory = $serviceFactory; $this->dataProcessor = $dataProcessor; $this->eavSetup = $eavSetup; $this->productAttributeManagement = $productAttributeManagement; + $this->attributeFactory = $attributeFactory; + $this->dataMerger = $dataMerger; + $this->resourceModelAttribute = $resourceModelAttribute; } /** * {@inheritdoc} * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA. + * @return DataObject|null */ public function apply(array $data = []): ?DataObject { + if (array_key_exists('additional_data', $data)) { + return $this->applyAttributeWithAdditionalData($data); + } + $service = $this->serviceFactory->create(ProductAttributeRepositoryInterface::class, 'save'); /** @@ -139,6 +172,26 @@ public function revert(DataObject $data): void ); } + /** + * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA. + * @return DataObject|null + */ + private function applyAttributeWithAdditionalData(array $data = []): ?DataObject + { + $defaultData = array_merge(self::DEFAULT_DATA, ['additional_data' => null]); + /** @var EavAttribute $attr */ + $attr = $this->attributeFactory->createAttribute(EavAttribute::class, $defaultData); + $mergedData = $this->dataProcessor->process($this, $this->dataMerger->merge($defaultData, $data)); + + $attributeSetData = $this->prepareAttributeSetData( + array_intersect_key($data, self::DEFAULT_ATTRIBUTE_SET_DATA) + ); + + $attr->setData(array_merge($mergedData, $attributeSetData)); + $this->resourceModelAttribute->save($attr); + return $attr; + } + /** * Prepare attribute data * diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php new file mode 100644 index 0000000000000..303ddd723d6c5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Fixture; + +use Magento\Catalog\Model\Category\Attribute; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\AttributeFactory; +use Magento\Eav\Model\ResourceModel\Entity\Attribute as ResourceModelAttribute; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class CategoryAttribute implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'is_wysiwyg_enabled' => false, + 'is_html_allowed_on_front' => true, + 'used_for_sort_by' => false, + 'is_filterable' => false, + 'is_filterable_in_search' => false, + 'is_used_in_grid' => true, + 'is_visible_in_grid' => true, + 'is_filterable_in_grid' => true, + 'position' => 0, + 'is_searchable' => '0', + 'is_visible_in_advanced_search' => '0', + 'is_comparable' => '0', + 'is_used_for_promo_rules' => '0', + 'is_visible_on_front' => '0', + 'used_in_product_listing' => '0', + 'is_visible' => true, + 'scope' => 'store', + 'attribute_code' => 'category_attribute%uniqid%', + 'frontend_input' => 'text', + 'entity_type_id' => '3', + 'is_required' => false, + 'is_user_defined' => true, + 'default_frontend_label' => 'Category Attribute%uniqid%', + 'backend_type' => 'varchar', + 'is_unique' => '0', + 'apply_to' => [], + ]; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + * @param AttributeFactory $attributeFactory + * @param ResourceModelAttribute $resourceModelAttribute + */ + public function __construct( + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository, + AttributeFactory $attributeFactory, + ResourceModelAttribute $resourceModelAttribute + ) { + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeFactory = $attributeFactory; + $this->resourceModelAttribute = $resourceModelAttribute; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + /** @var Attribute $attr */ + $attr = $this->attributeFactory->createAttribute(Attribute::class, self::DEFAULT_DATA); + $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)); + $attr->setData($mergedData); + $this->resourceModelAttribute->save($attr); + return $attr; + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Catalog/Test/Fixture/Product.php b/app/code/Magento/Catalog/Test/Fixture/Product.php index c6d0905c539ed..f856bff65a1b1 100644 --- a/app/code/Magento/Catalog/Test/Fixture/Product.php +++ b/app/code/Magento/Catalog/Test/Fixture/Product.php @@ -120,11 +120,7 @@ public function apply(array $data = []): ?DataObject public function revert(DataObject $data): void { $service = $this->serviceFactory->create(ProductRepositoryInterface::class, 'deleteById'); - $service->execute( - [ - 'sku' => $data->getSku() - ] - ); + $service->execute(['sku' => $data->getSku()]); } /** diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml new file mode 100644 index 0000000000000..86b3e942f3f46 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.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="AdminCheckProductByIdOnProductGridActionGroup"> + <annotations> + <description>Check the checkbox for the product on the Product Grid using Product ID</description> + </annotations> + <arguments> + <argument name="productId" type="string"/> + </arguments> + + <waitForElementClickable selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" stepKey="waitForElementClickable" /> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" stepKey="selectProduct"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml new file mode 100644 index 0000000000000..2907f88446ec2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.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="AdminDeleteCreatedColorSpecificAttributeActionGroup" > + <annotations> + <description>Delete the created new colors in color attribute</description> + </annotations> + <arguments> + <argument name="Color" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="Color" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{AdminProductAttributeGridSection.deleteSpecificColorAttribute(Color)}}" stepKey="deleteColor"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveTheDeletedColor"/> + <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml index 635e36c458519..7ca4d177ae4fa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml @@ -15,5 +15,6 @@ <conditionalClick selector="{{AdminProductGridTableHeaderSection.id('ascend')}}" dependentSelector="{{AdminProductGridTableHeaderSection.id('descend')}}" visible="false" stepKey="sortById"/> <waitForPageLoad stepKey="waitForPageLoad"/> + <wait time="5" stepKey="simpleWait" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index e5b6efbd6373a..57d5103a87a7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -31,6 +31,13 @@ <data key="is_active">true</data> <data key="include_in_menu">true</data> </entity> + <entity name="SimpleSubCat" type="category"> + <data key="name" unique="suffix">SubCat</data> + <data key="name_lwr" unique="suffix">simplesubcategory</data> + <data key="urlKey" unique="suffix">simplesubcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + </entity> <entity name="NewRootCategory" type="category"> <data key="name" unique="suffix">NewRootCategory</data> <data key="name_lwr" unique="suffix">newrootcategory</data> @@ -309,4 +316,4 @@ <var key="category_id" entityKey="id" entityType="category"/> <var key="sku" entityKey="sku" entityType="product"/> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml index e1072001b56e5..ebde9601149e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml @@ -43,6 +43,12 @@ <data key="filename">jpg</data> <data key="file_extension">jpg</data> </entity> + <entity name="GifImageWithUnusedTransparencyIndex" type="image"> + <data key="title" unique="suffix">GifImageWithUnusedTransparencyIndex</data> + <data key="file">transparency_index.gif</data> + <data key="filename">transparency_index</data> + <data key="file_extension">gif</data> + </entity> <entity name="LargeImage" type="image"> <data key="title" unique="suffix">largeimage</data> <data key="file">large.jpg</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 98085011dbc1c..6791c9ad7787f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -31,6 +31,29 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeLayered" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">textarea</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="backend_type">text</data> + <data key="is_wysiwyg_enabled">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> <entity name="productAttributeWithTwoOptions" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">select</data> @@ -134,7 +157,7 @@ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> - <data key="attribute_code">testattribute</data> + <data key="attribute_code" unique="suffix">testattribute</data> <data key="frontend_input">select</data> <data key="scope">global</data> <data key="is_required">false</data> @@ -299,7 +322,7 @@ <data key="frontend_input">date</data> <data key="is_required_admin">No</data> </entity> - <entity name="dropdownProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <entity name="dropdownProductAttribute" extends="productAttributeLayered" type="ProductAttribute"> <data key="frontend_input">select</data> <data key="frontend_input_admin">Dropdown</data> <data key="is_required_admin">No</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index c22677f0e0f5f..8b1a25adb267c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -23,7 +23,7 @@ <element name="expandRootCategoryByName" type="button" selector="//div[@class='x-tree-root-node']/li/div/a/span[contains(., '{{categoryName}}')]/../../img[contains(@class, 'x-tree-elbow-end-plus')]" parameterized="true" timeout="30"/> <element name="categoryByName" type="text" selector="//div[contains(@class, 'categories-side-col')]//a/span[contains(text(), '{{categoryName}}')]" parameterized="true" timeout="30"/> <element name="expandCategoryByName" type="text" selector="//span[contains(text(),'{{categoryName}}')]/ancestor::div[contains(@class,'x-tree-node-el')]//img[contains(@class,'x-tree-elbow-end-plus') or contains(@class,'x-tree-elbow-plus')]" parameterized="true" timeout="30"/> - <element name="subCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'SimpleSubCategory') and contains(text(),'({{productCount}})')]" parameterized="true"/> + <element name="subCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'SubCat') and contains(text(),'({{productCount}})')]" parameterized="true"/> <element name="defaultCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'Default Category') and contains(text(),'({{productCount}})')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml index aa86573044279..021057e06e7ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml @@ -25,7 +25,7 @@ <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> <element name="dropdownAddOptions" type="button" selector="#add_new_option_button" timeout="30"/> <element name="storefrontProperties" type="text" selector="//*[@id='product_attribute_tabs_front']/span[1]"/> - + <element name="useInSearchResultsLayeredNavigation" type="select" selector="#is_filterable_in_search"/> <!-- Manage Options nth child--> <element name="dropdownNthOptionIsDefault" type="checkbox" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) .input-radio" parameterized="true"/> <element name="dropdownNthOptionAdmin" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(3) input" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index e4b33ac795559..8e2877b47b64a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -23,6 +23,8 @@ <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> + <element name="addSelected" type="button" selector="//*[contains(text(),'Add Selected')]" timeout="30"/> + <element name="deleteSpecificColorAttribute" type="button" selector="//input[@value='{{var}}']/../..//button[@class='action- scalable delete delete-option']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 0a3c67bc00d5b..f0a1b0f2e9452 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -83,5 +83,9 @@ <element name="newAddedAttributeValue" type="text" selector="//option[contains(@data-title,'{{attributeValue}}')]" parameterized="true"/> <element name="country_Of_Manufacture" type="select" selector="//td[contains(text(), 'country_of_manufacture')]"/> <element name="textArea" type="text" selector="//textarea[@name='product[test_custom_attribute]']" timeout="30"/> + <element name="assignedSourcesQty" type="input" selector="//input[@name='sources[assigned_sources][0][quantity]']"/> + <element name="btnAdvancedInventory" type="button" selector="//button//span[text()='Advanced Inventory']/.."/> + <element name="saveCategory" type="button" selector="//button[@data-action='close-advanced-select']" timeout="30"/> + <element name="attributeRequiredInputField" type="select" selector="//select[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 05391d9babce5..28cd0ad14e2d5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -11,6 +11,7 @@ <element name="productRowBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> <element name="productRowByName" type="block" selector="//td[count(../../..//th[./*[.='Name']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> <element name="productRowCheckboxBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> + <element name="productRowCheckboxById" type="block" selector="#idscheck{{id}}" parameterized="true" /> <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml index 387e252ae93d4..acc88d0001775 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml @@ -11,5 +11,6 @@ <section name="StoreFrontRecentlyViewedProductSection"> <element name="ProductName" type="text" selector="//div[@class='products-grid']/ol/li[position()={{position}}]/div/div[@class='product-item-details']/strong/a" parameterized="true"/> + <element name="ProductPrice" type="text" selector=".price-including-tax .price"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index 526ac700a0b5a..744b279e4c77d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -44,5 +44,6 @@ <element name="productAttributeName" type="button" selector="//div[@class='filter-options-title' and contains(text(),'{{var1}}')]" parameterized="true"/> <element name="productAttributeOptionValue" type="button" selector="//div[@id='narrow-by-list']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="outOfStockProductCategoryPage" type="text" selector="//div[@class='stock unavailable']//span[text()='Out of stock']"/> + <element name="ListedProductAttributes" type="block" selector="//div[@aria-label='{{vs_attribute}}']//div[@aria-label='{{attribute_name}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 61e6a345b9ba5..de1c010797b6a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -35,5 +35,6 @@ <element name="ProductAddToCompareByName" type="text" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> <element name="ProductStockUnavailable" type="text" selector="//*[text()='Out of stock']"/> + <element name="listedProductOnProductPage" type="block" selector="//div[contains(@aria-labelledBy,'{{attribute_code}}')]//div[@aria-label='{{attribute_name}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml index 26a5452ee018c..6edef36fd98f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -27,5 +27,9 @@ <element name="expandPriceLayeredNavigationButton" type="button" selector="//div[@class='filter-options-title'][text()='Price']"/> <element name="seeLayeredNavigationFirstPriceRange" type="button" selector="//a//span[@class='price' and text()='${{minPrice}}']/..//span[@class='price' and text()='${{maxPrice}}']/..//span[@class='count' and text()=({{count}})]" parameterized="true"/> <element name="seeLayeredNavigationSecondPriceRange" type="button" selector="//a//span[@class='price' and text()='${{minPrice2}}']/../..//a[text()='{{maxPrice2}}']/..//span[@class='count' and text()=({{count}})]" parameterized="true"/> + <element name="seeLayeredNavigationCategoryTextSwatch" type="text" selector="//div[@class='filter-options-title' and contains(text(),'TextSwatch')]"/> + <element name="seeLayeredNavigationCategoryVisualSwatch" type="text" selector="//div[@class='filter-options-title' and contains(text(),'attribute')]"/> + <element name="seeTextSwatchOption" type="text" selector="//div[@class='swatch-option text ' and contains(text(),'textSwatchOption1')]"/> + <element name="seeVisualSwatchOption" type="text" selector="//div[@class='swatch-option image ']/..//div[@data-option-label='visualSwatchOption2']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 6ea8102a035d3..370487075c3c0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -30,7 +30,10 @@ <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> + <element name="productOptionSelectByColor" type="select" selector=".//option[text()='Choose an Option...']/../../select" /> <element name="asLowAs" type="input" selector="span[class='price-wrapper '] "/> + <element name="asLowAsLabel" type="input" selector="//strong[@id='block-related-heading']/following::span[@class='price-label'][1]"/> + <element name="asLowAsLabelAgain" type="input" selector="//strong[@id='block-related-heading']/following::span[@class='price-label'][2]"/> <element name="specialPriceValue" type="text" selector="//span[@class='special-price']//span[@class='price']"/> <element name="mapPrice" type="text" selector="//div[@class='price-box price-final_price']//span[contains(@class, 'price-msrp_price')]"/> <element name="clickForPriceLink" type="text" selector="//div[@class='price-box price-final_price']//a[contains(text(), 'Click for price')]"/> @@ -106,7 +109,7 @@ <element name="customOptionHour" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='hour']" parameterized="true"/> <element name="customOptionMinute" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='minute']" parameterized="true"/> <element name="customOptionDayPart" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='day_part']" parameterized="true"/> - + <element name="swatchOptionDisabled" type="text" selector=".//*[@class='swatch-option color disabled']"/> <element name="addToCartEnabled" type="button" selector="#product-addtocart-button:not([disabled])"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml index 38d8b572ac62d..34603cbc6b3c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-26780"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml index f5dec88789bf0..cffeb9006d448 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml @@ -21,7 +21,10 @@ <before> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPageBefore"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> <!--Create Category--> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -33,7 +36,8 @@ <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteCreatedAttribute"> <argument name="ProductAttribute" value="newProductAttribute"/> </actionGroup> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -97,7 +101,11 @@ <!--Click on Go Back button --> <click selector="{{AdminProductFormActionSection.backButton}}" stepKey="clickBackToGridSimple"/> - + <!--Clear filter if available --> + <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector="{{AdminGridFilterControls.clearAll}}" visible="true" stepKey="clearTheFiltersIfPresent"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProductOnProductGridPage"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> <!-- Select created attribute as an column --> <actionGroup ref="ToggleAdminProductGridColumnsDropdownActionGroup" stepKey="openColumnsDropdown"/> <actionGroup ref="CheckAdminProductGridColumnOptionActionGroup" stepKey="checkCreatedAttributeColumn"> @@ -106,6 +114,9 @@ <wait stepKey="waitPostClickingCheck" time="5"/> <actionGroup ref="ToggleAdminProductGridColumnsDropdownActionGroup" stepKey="closeColumnsDropdown"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> <!-- Asserting the value of the created column --> <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeCreatedAttributeColumn"> <argument name="row" value="1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml index b677fae5e58ea..b5068e5b0b37e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-9143"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml index 35f17f1dcb32a..af67fc0e9b82e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26919"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index bed5297041dd1..ef4b1879dde52 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-113"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml index d713660d7ee63..412fa19a295a5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-103"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml index e4cf255a03e05..b62aa752449a0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-188"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index 5786eabf9c840..4f2dd66e4d4ff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11065"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml index 471880b5f6ea5..c53234ab3f56b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-28810"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml index a3d50a9c361b8..31ff50c0a0ef1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml @@ -16,6 +16,7 @@ <description value="Test verifies the process of deleting product image"/> <severity value="MAJOR"/> <testCaseId value="AC-4473"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml index f5cf4cd3f2417..5acd0fa09b4e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-168"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml index 8d59e475ca10c..6d3eddbfed5b5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml index 1dec1073f56ee..40b6ddeaff506 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml index c682c7ab4001e..67ceaf89c01db 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26810"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a custom attribute set and custom product attribute --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml index 68e6040277247..8aea1a0c1cd39 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-35612"/> <useCaseId value="MC-31892"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml index f2413a1523394..b1911645cf00d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13749"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index 75f805bb99e04..3bf36ca9e9486 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13638"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml index 97992c35b7316..a30209128e7d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13637"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index 88d24540b11f8..dac214f03d456 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13636"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml index ce4cb250796bd..901b8973c8e2e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml @@ -17,6 +17,7 @@ <group value="catalog"/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml index 92a3b298aa6b6..b460c8125c221 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml @@ -19,6 +19,7 @@ <severity value="MAJOR"/> <group value="pr_exclude"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml index c15cedadb4460..9892b13b36984 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11064"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index f956c73319425..45506ee1cc8a2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml index b23ce827d5d69..668796ff95808 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml @@ -16,6 +16,7 @@ Product List page filter grid by created product, add mentioned columns to grid, check values."/> <group value="catalog"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml index 3fdd278a6bacd..fe7426bad856c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml @@ -20,6 +20,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-6078"/> <useCaseId value="ACP2E-1018"/> + <group value="cloud"/> </annotations> <before> <!-- Configure Stores -> Configuration -> Catalog -> Catalog -> Price Scope = Website --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 2cdec1405e9f9..26faf7bb42a48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13635"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml new file mode 100644 index 0000000000000..d326204999c7d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest"> + <annotations> + <features value="Bundle"/> + <stories value="Create Admin checks if only one Quantity Configuration is displayed for Bundle product while creating an order"/> + <title value="Admin checks if only one Quantity Configuration is displayed for Bundle product while creating an order"/> + <description value="create Admin checks if only one Quantity Configuration is displayed for Bundle product "/> + <severity value="MAJOR"/> + <testCaseId value="AC-5237"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Go to bundle product creation page --> + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> + <waitForPageLoad stepKey="waitForBundleProductCreationPage"/> + <!-- Entering Bundle Product name,SKU, category, url key --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="fillProductName"/> + <!-- Create bundle product options --> + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption3"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> + <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> + <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow2"/> + <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> + <!--Save the product--> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <!--Create new order--> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="Simple_US_Customer_NY"/> + </actionGroup> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearch"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> + <waitForPageLoad stepKey="waitForProductLoad"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml index a865cbfdef22c..384e3d9362c77 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3241"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml index c42569385c59a..bf1835c03fb7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10925"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml index 7191f1971b319..6406c914f40f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10928"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="virtual"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml index 19064458ae2a4..472d2a826a8f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10884"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml index f2fcc7a3e8905..3408ba9827397 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-27423"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml index 9ec19ee97eed0..3ab36faf8d01f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-95797"/> <group value="category"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml index 852353300d090..4190adbf6b3b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89024"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml index 83404391abca9..fe722f73e8508 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-72102"/> <group value="category"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml index 6f183a44d8277..2958a76f4836e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26112"/> <group value="catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml new file mode 100644 index 0000000000000..3d640eb8e1be6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryWithAPIForMultiStoresTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create Category Using API post"/> + <description value="Create Category Using API post when there are more than stores existing"/> + <testCaseId value="AC-5384"/> + <severity value="MAJOR"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <!--Create a new additional store view for the default website and store--> + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createNewSecondStoreviewForDefaultStore"> + <argument name="storeView" value="SecondStoreGroupUnique"/> + </actionGroup> + <!--Create a new second store for the default website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStoreForMainWebsite"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <!--Create a store view for the second store--> + <actionGroup ref="CreateCustomStoreViewActionGroup" stepKey="createStoreviewForSecondStore"/> + <!--Create a second custom website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createNewWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!--Create a store for the second website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createStoreForNewWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <!--Create a store view of the new store of second website--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="staticSecondStore"/> + </actionGroup> + </before> + + <after> + <!--Delete the created category--> + <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <!--Set the main website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setMainWebsiteAsDefault"> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <!--Delete the second created website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteCreatedWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <!--Create a second store created for main website--> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedCustomWebsiteStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <!--Create a second store view created for main website--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCreatedCustomStoreview"> + <argument name="customStore" value="SecondStoreGroupUnique"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Create a category and check that in storefront --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnStorefront" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad"/> + <!--Switch to second store view and check that created category in storefront--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToSecondMainStoreView"> + <argument name="storeView" value="SecondStoreGroupUnique"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory2"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondMainStoreView" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad2"/> + <!--Switch to second store and check that created category in storefront--> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToSecondMainStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory3"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondMainStore" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad3"/> + <!--Switch to second website and check that created category in storefront--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setNewWebsiteAsDefault"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage2"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory4"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondWebsite" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index adb9d9bd824f0..68c2e80e5dd95 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml index a711228e659b1..9812f64660816 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml index 6d7d56861b731..440d0b5ee0486 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml index f60312f19a7e0..d8775fd307003 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml index 31ad92afb9d4f..135d4a7ac3c3b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml index 3f51fa2296219..877a084aa24c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-21451"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml index 6f97cc7abe71f..ea5209dce6f33 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml @@ -15,6 +15,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-4982"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index 5931193dbe7ca..75cab8bd71851 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10827"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml index 5c00028ee69ba..d14f1c981537e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index 45b776a6c8713..e8ea05c240c18 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10828"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml index 52cac23574b53..d8af1d3194d12 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="https://github.com/magento/magento2/pull/25132"/> <severity value="CRITICAL"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml index e5251b5fee406..aef4c7497de64 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-12296"/> <useCaseId value="MAGETWO-59055"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml index fc5fa60f754c4..b603c1a1252e6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-170"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a custom attribute set and custom product attribute --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 90730a6516d39..41604104003ec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10899"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index da87880477de5..2e50911f76559 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10906"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml index 7a087f02a3fff..290a1991c825f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-42510"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml index 686f8aa865c22..1fdc7e1563823 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-42510"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml index d129ad3a04d0f..160e7e8908d97 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-244"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml index c18f1c69f87ff..7ca613ec00118 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-5472"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml index e61684b91c082..580f30e3548af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-112"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml index 7df525f5f9f2f..e7df46287282d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-46142"/> <group value="category"/> + <group value="cloud"/> </annotations> <!--Delete all created data during the test execution and assign Default Root Category to Store--> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml index f7f68b9f85c04..2f724aab7877e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml @@ -19,6 +19,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml index 819835dead304..1bfe63748bf0a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89023"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml index a18754f0ecb18..501a6dbb1e240 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="AC-2928"/> <useCaseId value="ACP2E-420"/> <group value="product"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml index 4b40f04f098e0..d3174a11d6346 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-89912"/> <group value="product"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml index 4ef9e2ec1fc62..d303dbb63c91c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-23414"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml index b1f18a770ea0b..e7c53a72bb1ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89910"/> <group value="product"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml index d74b15b01ea3b..39d6b45e4e595 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-36852"/> <severity value="MAJOR"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 494ff1008e6ef..a4cab3cad7b5f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR" /> <testCaseId value="MC-105"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml index 68b9c6b32c981..0ad6d399bdb73 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-27471"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml index 18fb840202f47..f181092030c22 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4529"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml index 4c02c57dae535..23f3ff0566eec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 71665e4064d55..b425968b36ed2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index 81d897d4836a5..faed5ea16810d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml index 7aeb1a1397952..d71247647caa2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -24,6 +24,8 @@ </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductGridPage" /> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteProducts" /> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml index b82c6ba13550c..3c01e43e85d43 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10889"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml index e26a42006b0ac..034af7e8b2147 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13684"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set Display Out Of Stock Product --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml index d4c7e20223a68..ed9104bf08594 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26728"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml index 841b08e70fb4f..bf11030cce59f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10885"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml index abbc541fbbcf3..1489df34ac3d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10887"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml index f43048f00e6b1..e71289b59eaa1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11015"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index 6712bf90c4700..740aa54aab502 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11466"/> <useCaseId value="MC-15391"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml index ae92e997e0aa0..a7c63e15789d0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 92b190efc6210..8f6ce45bfd538 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -17,6 +17,7 @@ <group value="Catalog"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index 900c40dec14da..b88b6d5d75757 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml index 2034ea8ec8211..3038f1fe468f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-11013"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml index b7e037b323ee2..bbb6d73d23219 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10893"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml index a6cd3c8b52b23..ed88c80619a6a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10886"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml index cb2e6b8e483a0..efbc198b05a82 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11014"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml index 744dbcc32e7fd..6d88ea477a883 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-37121"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml index db932e5d4751f..11f97edf5019d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-19716"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index 83e9a70ad285f..3f180ed0369dc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -16,6 +16,7 @@ <description value="Admin are able to change Input Type of Text Editor product attribute"/> <severity value="BLOCKER"/> <testCaseId value="MC-6215"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index 2b1ba0894ecd0..ce56fa18dee2b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-37347"/> <group value="catalog"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml index 9f757ff72d067..56e13e84eee58 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-48850"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index f10288bea36d9..997c66d19098a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16472"/> <useCaseId value="MC-17332"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index 82a9a610e32c9..492310d457bf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16471"/> <useCaseId value="MAGETWO-70232"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create category--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml index fe13603fa6154..f6ec18e70a8ed 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-29179"/> <group value="catalog"/> <group value="asynchronousOperations"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="createFirstProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index c8aba75838f52..08d7c5744acbd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -45,12 +45,12 @@ <argument name="keyword" value="api-simple-product"/> </actionGroup> - <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox1"> - <argument name="product" value="$$createProductOne$$"/> + <actionGroup ref="AdminCheckProductByIdOnProductGridActionGroup" stepKey="clickCheckbox1"> + <argument name="productId" value="$$createProductOne.id$$"/> </actionGroup> - <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox2"> - <argument name="product" value="$$createProductTwo$$"/> + <actionGroup ref="AdminCheckProductByIdOnProductGridActionGroup" stepKey="clickCheckbox2"> + <argument name="productId" value="$$createProductTwo.id$$"/> </actionGroup> <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickDropdown"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml index 2bcabbd54f49c..d73d13eafd77d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10022"/> <useCaseId value="MAGETWO-89248"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simpleSubCategoryOne"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index 96a8f711ea569..256768848efb6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <features value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index efd2a54fc5133..e22b20cdad263 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -16,6 +16,7 @@ <features value="Catalog"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 14636d8b8ae3d..1cf398d852c50 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <features value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml index 9eba952c1a3b2..18d2b8bed5799 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index 203ed2c530fbf..d272aed83f9cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-25783"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml index 4f3feba01a92c..84770061c12c8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-8902"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml index f3981e7b8f76a..d3b8113891d09 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml @@ -16,6 +16,7 @@ <description value="Test whenever HTML tags are allowed for a product attribute label"/> <severity value="CRITICAL"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index 85fec54de2f0c..25a18074689da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -94,8 +94,8 @@ <!-- 4. Run cron to reindex --> <wait time="60" stepKey="waitForChanges"/> - <magentoCLI command="cron:run --group index" stepKey="runCron"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice"/> + <magentoCron groups="index" stepKey="runCron" /> + <magentoCron groups="index" stepKey="runCronTwice" /> <!-- 5. Open category A on Storefront again --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCategoryA"/> @@ -124,8 +124,8 @@ <!-- 8. Run cron reindex (Ensure that at least one minute passed since last cron run) --> <wait time="60" stepKey="waitOneMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron1"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice1"/> + <magentoCron groups="index" stepKey="runCron1" /> + <magentoCron groups="index" stepKey="runCronTwice1" /> <!-- 9. Open category A on Storefront again --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshCategoryAPage"/> @@ -180,8 +180,8 @@ <!-- 14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> <wait time="60" stepKey="waitMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron2"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice2"/> + <magentoCron groups="index" stepKey="runCron2" /> + <magentoCron groups="index" stepKey="runCronTwice2" /> <!-- 15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="onPageCategoryB"> @@ -240,8 +240,8 @@ <!-- 17.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> <wait time="60" stepKey="waitForOneMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron3"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice3"/> + <magentoCron groups="index" stepKey="runCron3" /> + <magentoCron groups="index" stepKey="runCronTwice3" /> <!-- 17.15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openPageCategoryB"> @@ -302,8 +302,8 @@ <!-- 18.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> <wait time="60" stepKey="waitExtraMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron4"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice4"/> + <magentoCron groups="index" stepKey="runCron4" /> + <magentoCron groups="index" stepKey="runCronTwice4" /> <!-- 18.15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="navigateToPageCategoryB"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index ef44d0b418b44..ca77574edfccd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6443"/> <useCaseId value="MAGETWO-90331"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml index d677eda5b0920..29819f4151fca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-19031"/> <testCaseId value="MC-20329"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin and delete all products --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml index 449d201393206..6268bc4e65ca8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92019"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml index 30c1b9296553a..d3008c2fbb397 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="ACP2E-258"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index bfa80c2e24b48..3d6a246b27b45 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml index 410e945cea7e5..a3f95eaeba5d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92424"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml index 99fe4dd0c135d..33a6a873d444e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-11512"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml index 521256cf57dd5..5720723ae4466 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-195"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml index 4a544b60f15b6..3854d34f56951 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-197"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index c5b475f616b7b..4759f97a9d47b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94265"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create 2 websites (with stores, store views)--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml index 8033a2dffec7c..aced4c8786745 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-212"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml index de3e2a9b9dada..2fdf988c48d6c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26720"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml index f647492775b2f..8ece4af9eaab8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml @@ -16,6 +16,7 @@ <description value="Admin Rename Category on Store View level"/> <severity value="MAJOR"/> <testCaseId value="AC-4284"/> + <group value="cloud"/> </annotations> <before> <!-- log in as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 49add85f76806..041f690e627c6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94330"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index 92818c846fcf1..3a0b78a0fa81c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17229"/> <useCaseId value="MAGETWO-69893"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml new file mode 100644 index 0000000000000..c4df428371db2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSaveProductByCustomDateWithCustomDateAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Adding Custom Date Attribute To Products"/> + <title value="Issue while saving the date type product attribute"/> + <description value="When we add the 01/01/1970 to the product attribute of the custom date type, it is throwing an error."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8290"/> + <useCaseId value="ACP2E-1749"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="dateProductAttribute"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="resetGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Generate date for use as default value, needs to be MM/d/YYYY and mm/d/yy --> + <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> + + <!-- Navigate to Stores > Attributes > Product. --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + + <!-- Create new Product Attribute as Date, with code and default value. --> + <actionGroup ref="CreateProductAttributeWithDateFieldActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="dateProductAttribute"/> + <argument name="date" value="{$generateDefaultDate}"/> + </actionGroup> + + <!-- Go to default attribute set edit page --> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <!-- Assert created attribute in unassigned section --> + <see userInput="{{dateProductAttribute.attribute_code}}" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> + <!-- Assign attribute to product group --> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <!-- Assert attribute in a group --> + <see userInput="{{dateProductAttribute.attribute_code}}" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <!-- Save attribute set --> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="SaveAttributeSet"/> + + <!-- Open Product Edit Page and set custom attribute value 01/01/1970 and save the product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.newAddedAttributeInput(dateProductAttribute.attribute_code)}}" userInput="01/01/1970" stepKey="fillCustomDateValue"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <!-- Open Product Index Page and filter the product by date 01/01/1970 --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> + <actionGroup ref="FilterProductGridByCustomDateRangeActionGroup" stepKey="filterProductGridByCustomDateRange"> + <argument name="code" value="{{dateProductAttribute.attribute_code}}"/> + <argument name="date" value="1/01/1970"/> + </actionGroup> + <!-- Check products filtering and see the product custom date 01/01/1970 successfully appeared --> + <see selector="{{AdminProductGridSection.productGridNameProduct($createProduct.name$)}}" userInput="$createProduct.name$" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml index 6e0ad56f0d5df..35e34fd8cb2ec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <useCaseId value="MC-38156"/> <testCaseId value="AC-1337"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml new file mode 100644 index 0000000000000..2877313e744ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductGifWithUnusedTransparencyImageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Using a GIF image with transparency color declared but not used as a product main image should not prevent the product grid from being rendered properly"/> + <title value="Using a GIF image with transparency color declared but not used as a product image"/> + <description value="Using a GIF image with transparency color declared but not used as a product main image should not prevent the product grid from being rendered properly"/> + <severity value="CRITICAL"/> + <useCaseId value="ACP2E-1632"/> + <testCaseId value="AC-8028"/> + <group value="Catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Navigate to the product grid and edit the product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProducForEditByClickingRow1Column2InProductGrid"/> + + <!-- Set the test GIF image as a main product image and save the product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForProduct"> + <argument name="image" value="GifImageWithUnusedTransparencyIndex"/> + </actionGroup> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> + + <!-- Go back to the product grid and make sure the product is present and visible on the grid --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="returnToProductIndex"/> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertFirstOnAdminGrid"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="resetProductGridBeforeLeaving"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml index 38ba4f4331c1d..99f8c3d9b78f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3411"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml index 96a6dc7b70a7a..17a8285ca5a4a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml index a8eddeb8b613f..55dcb87390b43 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml new file mode 100644 index 0000000000000..554f8e2448b89 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.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="AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded"> + <annotations> + <features value="Catalog"/> + <stories value="Related Products"/> + <title value="Test for Related Products Price Box is not being updated when not needed"/> + <description value="Test for Related Products Price Box"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4411"/> + <group value="Catalog"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <!-- Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create an attribute with two options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption1"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption2"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild4"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild5"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild6"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <!-- Delete Created Data –>--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProduct1" stepKey="deleteConfigProduct1"/> + <deleteData createDataKey="createConfigProduct2" stepKey="deleteConfigProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreatedProductEditPageActionGroup" stepKey="openCreatedProductEditPage"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <!-- Select createConfigProduct1 in AddRelatedProduct--> + <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="selectcreateConfigProduct1"> + <argument name="sku" value="$$createConfigProduct1.sku$$"/> + </actionGroup> + <!-- Select createConfigProduct2--> + <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="selectcreateConfigProduct2"> + <argument name="sku" value="$$createConfigProduct2.sku$$"/> + </actionGroup> + <!--Save the createConfigProduct--> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="savecreateConfigProduct"/> + <!-- Go to frontend and open createConfigProduct on Main website --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="opencreateConfigProduct"> + <argument name="productUrl" value="$$createConfigProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Check Product Page is opened and contains Related Product Block and its products--> + <actionGroup ref="StorefrontAssertRelatedProductOnProductPageActionGroup" stepKey="verifycreateConfigProduct1"> + <argument name="productName" value="$createConfigProduct1.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertRelatedProductOnProductPageActionGroup" stepKey="verifycreateConfigProduct2"> + <argument name="productName" value="$createConfigProduct2.name$"/> + </actionGroup> + <scrollTo selector="{{AdminProductFormSection.footerBlock}}" stepKey="scrollToFooter"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <!-- Assert Configurable Product Price--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabel}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProduct"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabelAgain}}" stepKey="grabProductPriceSecond"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductSecond"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <scrollToTopOfPage stepKey="scrollToTopOfPage5"/> + <selectOption userInput="option1" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption3"/> + <waitForPageLoad time="30" stepKey="waitForPreviewLoad"/> + <scrollTo selector="{{AdminProductFormSection.footerBlock}}" stepKey="scrollToFooterAgain"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabel}}" stepKey="grabProductPriceAgain"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductAgain"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabelAgain}}" stepKey="grabProductPriceAgainAgain"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductAgainAgain"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> +</test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml index 7989de271b3ad..6d9b91815034a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-97050"/> <useCaseId value="MAGETWO-96842"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml index bb6098f55cf96..367dba6500d3b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-194"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml index bb7aca5ed7706..a1063bbc2402a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index ea50a17b47b44..360f3aa14a7a3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index f8c3857fb5442..6532136985352 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 4389bf4bd6383..d166d22b804de 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92338"/> <group value="category"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCategory"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml index c04212a220f4a..0c40a411a5af0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml index 051495b257012..e3026fcee6c27 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml index 3c4dd60785614..c880e232b8fc2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27207"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 8b2d447d297d0..2e8007946b866 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index 0fd564d86f03f..edf1ab7910b14 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index ad14bc274a52d..93f041f172720 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index 2e72bb734fe00..067453be59978 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index 669e5cd040c5e..511cb4af97d72 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index fa9aea7683200..adf294c954ba3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -18,6 +18,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 4431991fdbb78..4759ed35181ce 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 01feac998060b..c41c00c6665b3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 214ca0e9b8576..bea04c1714de6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index b436601356b31..44a074166189a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml index 607ebd1a626a2..cab3f33e516b4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml index 27b65c53b835b..a3050e015a930 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index b0c14bcb79e18..3df499905ea2b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml index 71cb86e765fd5..7c066343ee7f3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -28,6 +29,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="selectAndDeleteProducts"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml index c79567d7f674f..b59531810d594 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml @@ -16,6 +16,7 @@ <description value="Category Selector limit category more than 5 from the root"/> <severity value="MAJOR"/> <testCaseId value="AC-4948"/> + <group value="cloud"/> </annotations> <before> <!-- Create six level nested category --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml index b989450ea2288..f921187cda63b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-30362"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index 30771fcfd947b..3d912906b4cbf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-134"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 5115399db9e3b..a85dbe696bad1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-132"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index cacf4f3f4c9f5..8a8ef7fdf6d7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-136"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml index 6ca81ee494730..7754bfe14e805 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-135"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index dc6409043f67f..3d892c2b16213 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-133"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescriptionAndUnderscoredSku" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml index 64da7e8599d07..e23f3472d8dc7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-163"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml index 12056962bac23..972616cad853e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-137"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml index 68a69644d3d7b..cbedba0127ebc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-165"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml index f6cfb58bf71df..a2db74321df2e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-164"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml index 132e82d49085e..cbc39a7e80bea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-162"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescriptionAndUnderscoredSku" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index ebc7bcd542a65..fbdfc3df7b6b0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -147,8 +147,10 @@ <see userInput="You saved the rule." stepKey="RuleSaved"/> <!--Create new order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml index bc9da6efcbf42..887894619706f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92229"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml index b983112d91e4b..5e1da0f77eb8a 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml @@ -15,6 +15,7 @@ <description value="Admin Can Create Category Anchor setting and it should work perfectly"/> <severity value="MAJOR"/> <testCaseId value="AC-4587"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategoryA"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml index 87dfca735cc0a..a7923f49d3f85 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26021"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml index b756df331d0c5..af637bf1bba58 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10898"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml index 72d3fa04591c2..2c357d120e11e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10888"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml index c7b9613e1ee48..7eaec8bab78fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10897"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml index 629d084b2617c..29073927dcba5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10894"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml index 8acb0bef4c438..c101d4bae0773 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-37806"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index 18869e670f62f..c9b221f414e94 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-46344"/> <group value="testNotIsolated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategoryC"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml index c062b9bc2f949..2e44ad45edf67 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-45666"/> <group value="catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Create category, flush cache and log in --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml index 123a686e421c1..87d5f01d093f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4341"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 3b0fad592fed8..7a10c0e949e31 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -18,13 +18,16 @@ <testCaseId value="MAGETWO-87014"/> <group value="pr_exclude"/> </annotations> + <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> + </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!--Login to Admin Area--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> - <!--Admin creates product--> <!--Create Simple Product--> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageSimple"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml index 9c18ba6cd654b..3b14122289d2b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml @@ -16,6 +16,7 @@ <description value="The product attribute that has no value should output 'N/A' on the product comparison page."/> <severity value="MINOR"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml similarity index 98% rename from app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml rename to app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml index f32ba620732fc..6ed898d5dee49 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml @@ -17,6 +17,8 @@ <severity value="BLOCKER"/> <testCaseId value="MC-25687"/> <group value="product"/> + + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -100,6 +102,7 @@ <executeJS function="window.scrollTo({top: {$sectionPosition}-{$floatingHeaderHeight}})" stepKey="scrollToOptions"/> <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> <waitForElementVisible selector=".admin__dynamic-rows[data-index='values'] tr.data-row" stepKey="waitForRowsToBeVisible"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear" /> <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml new file mode 100644 index 0000000000000..2c8d91d997177 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SavingCustomAttributeValuesUsingUITest"> + <annotations> + <group value="Custom Attribute"/> + <stories value="Create Customer Attribute with Multi Select Input Type"/> + <title value="Saving custom attribute values using UI"/> + <description value="Saving custom attribute values using UI"/> + <severity value="MAJOR"/> + <testCaseId value="AC-7325"/> + <group value="cloud"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Create Simple Product --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"/> + <!--Navigate to Stores > Attributes > Product.--> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="multiselectProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption1"> + <argument name="adminName" value="{{multiselectProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption2"> + <argument name="adminName" value="{{multiselectProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption3"> + <argument name="adminName" value="{{multiselectProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setDropdownUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (with results)"/> + </actionGroup> + <selectOption selector="{{AttributePropertiesSection.useInSearchResultsLayeredNavigation}}" userInput="Yes" stepKey="selectUseInLayeredNavigationOption"/> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <magentoCron groups="index" stepKey="reindex"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!--Log out--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + </after> + + <!-- Open created product for edit --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createSimpleProduct.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForAttributeAdded"/> + <!-- Filter By Attribute Label on Add Attribute Page --> + <click selector="{{AdminProductFiltersSection.filter}}" stepKey="clickOnFilter"/> + <fillField selector="{{AdminProductAddAttributeModalSection.attributeCodeFilter}}" userInput="{{multiselectProductAttribute.attribute_code}}" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click stepKey="clickonFirstRow" selector="{{AdminProductAddAttributeModalSection.firstRowCheckBox}}"/> + <click stepKey="clickOnAddSelected" selector="{{AdminProductAttributeGridSection.addSelected}}"/> + <waitForPageLoad stepKey="waitForAttributeAdded2"/> + <!-- Expand 'Attributes' tab --> + <actionGroup ref="AdminExpandProductAttributesTabActionGroup" stepKey="expandAttributesTab"/> + <!-- Check created attribute presents in the 'Attributes' tab --> + <seeElement selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" stepKey="assertAttributeIsPresentInTab"/> + <!-- Select attribute options --> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_admin}}" stepKey="selectProduct1AttributeOption"/> + <!-- Save product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> + <!-- Go to Storefront and search for product--> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + <!-- Assert custom Attribute in Layered Navigation--> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(multiselectProductAttribute.attribute_code)}}" stepKey="waitForAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(multiselectProductAttribute.attribute_code)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForAttributeOptionsVisible"/> + <wait time="10" stepKey="Wait"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="{{multiselectProductAttribute.option1_frontend}}" stepKey="seeOption2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml index 902c4339cf208..2752aeeadd3b2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-248"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- log in as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml index 90d432b70f794..d8b5af6694d75 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml @@ -15,6 +15,7 @@ <description value="Apply discount tier price and custom price values for simple product"/> <severity value="MAJOR"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml index ef569a56a3fed..c4e64a563f065 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <description value="Customer should be able to add products to Cart if product qty less or equal 0 and Backorders are allowed on Product level"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index fb4bd4d1dcb74..bacdf5c8f695f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="theme"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml index acceb6662d59e..4a28581f22840 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-19626"/> <useCaseId value="MAGETWO-98748"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml index 973fcb68b63e9..992a87016619b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml index c9be526e095aa..11a6228d81f45 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17386"/> <useCaseId value="MC-15341"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml index d0c6c4fe86aee..6300d26fe2383 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml @@ -17,6 +17,7 @@ (visible and active) for each selected option for the configurable product"/> <group value="catalog"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml index 66f900293dd1c..c6ee3aee3c29d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6484"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create product with description --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml index 58e6ee43ce744..6f71809249332 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml index f9ad2d69264f4..367fb8143c471 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml @@ -15,6 +15,7 @@ <description value="Check arrows next to the thumbs are not visible than there is room for all pictures."/> <severity value="BLOCKER"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml new file mode 100644 index 0000000000000..80919fd06c4f2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductImageSlideTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Image"/> + <title value="Product image should be visible and slide left or right on frontend in mobile"/> + <description value="Product image should be visible and slide left or right on frontend in mobile"/> + <group value="Catalog"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8441"/> + </annotations> + <before> + <resizeWindow width="800" height="700" stepKey="resizeWindowToMobileView"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct"> + <argument name="sku" value="{{SimpleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </after> + + <!--Create product--> + <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="openNewProductPage"/> + <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillSimpleProductMain"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + + <!-- Add image to product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForSimpleProduct"> + <argument name="image" value="TestImageWithDotInFilename"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForSimpleProduct2"> + <argument name="image" value="TestImageWithDotInFilename"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + + <!-- Assert product in storefront product page --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageActionGroup" stepKey="assertProductInStorefrontProductPage"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + + <click selector="{{StorefrontProductMediaSection.fotoramaImageThumbnail('2')}}" stepKey="clickForFullScreenImage1"/> + <wait stepKey="waitForImageScroll" time="2"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="waitPrevButton"/> + <seeElement selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="seePrevButton"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml index a711a585a81b0..e16e079afd1f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml @@ -16,6 +16,7 @@ <description value="Product image with dot in filename should be visible on frontend after catalog image cache flush"/> <group value="Catalog"/> <severity value="AVERAGE"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set system/upload_configuration/enable_resize 0" stepKey="disableImageResizing"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index e6aea3e7b3321..64705ff725922 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="product"/> <testCaseId value="MAGETWO-92384"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml index 8bbe9b137abbb..c86331eba9631 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="product"/> <testCaseId value="MAGETWO-93794"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategoryOne"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml index 95072f81e02b8..79c110775f2e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-91893"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> @@ -38,7 +39,7 @@ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="chooseDefaultAttributeSet"/> <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> - <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('testattribute')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> + <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('$$createProductAttribute.attribute_code$$')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> <click selector="{{AttributeSetSection.Save}}" stepKey="saveAttributeSet"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> <seeElement selector=".message-success" stepKey="assertSuccess"/> @@ -49,6 +50,6 @@ <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.urlKey)}}" stepKey="goProductPageOnStorefront"/> <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <dontSeeElement selector="//table[@id='product-attribute-specs-table']/tbody/tr/th[contains(text(),'testattribute')]" stepKey="seeAttribute2"/> + <dontSeeElement selector="//table[@id='product-attribute-specs-table']/tbody/tr/th[contains(text(),'$$createProductAttribute.attribute_code$$')]" stepKey="seeAttribute2"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml index 5f5279cc483c1..c7c4bb096c79e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-2076"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml index d56faf9d5dec4..e96290b326ed1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-91960"/> <group value="productCompare"/> + <group value="cloud"/> </annotations> <before> <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createProductAttribute"/> @@ -41,7 +42,7 @@ <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="chooseDefaultAttributeSet"/> <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> - <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('testattribute')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> + <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('$$createProductAttribute.attribute_code$$')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> <click selector="{{AttributeSetSection.Save}}" stepKey="saveAttributeSet"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> <seeElement selector=".message-success" stepKey="assertSuccess"/> @@ -81,6 +82,6 @@ <argument name="productVar" value="$$createSimpleProduct1$$"/> </actionGroup> <seeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'SKU')]" stepKey="seeCompareAttribute1"/> - <dontSeeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'testattribute')]" stepKey="seeCompareAttribute2"/> + <dontSeeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'$$createProductAttribute.attribute_code$$')]" stepKey="seeCompareAttribute2"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index deafab6a95258..2481e8aad9f42 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-16462"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index ba7388ebb1ccc..e62c83e6666f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25479"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create Simple Product with Custom Options--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml index 9e01caa0f1d32..7e23dc67d2f06 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94210"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml index e19446c157605..5a62352e8d50c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <group value="Catalog"/> <testCaseId value="MC-35068"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="defaultCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml index 9821121d8c171..57de0d2c9a95f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml @@ -20,6 +20,7 @@ to selected needed option."/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!-- Select first option using product query params URL --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml index 5ff0a002e11ed..8ba09a80f0c21 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-97508"/> <useCaseId value="MAGETWO-96847"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml new file mode 100644 index 0000000000000..6c79b40b3ff94 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyCategoryPageNotCachedTest"> + <annotations> + <features value="Catalog"/> + <title value="Verify category page is not cached"/> + <stories value="Product Categories Indexer"/> + <description value="Verify that the category page is NOT cached for customers with different tax rates"/> + <severity value="AVERAGE"/> + <group value="Catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!--Login to Admin Panel--> + <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> + <!-- Create tax rate for CA --> + <createData entity="US_CA_Rate_1" stepKey="createTaxRateCA"/> + <!-- Create tax rate for TX --> + <createData entity="ThirdTaxRateTexas" stepKey="createTaxRateTX"/> + <!-- Create Tax Rules --> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule1"> + <argument name="taxRate" value="$$createTaxRateCA$$"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule2"> + <argument name="taxRate" value="$$createTaxRateTX$$"/> + <argument name="taxRule" value="SimpleTaxRule2"/> + </actionGroup> + <!--Create Customers--> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomerCA"/> + <createData entity="Simple_US_Customer" stepKey="createCustomerTX"/> + <!--Create Category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create Products--> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <field key="price">100</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <field key="price">200</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Display product price including and excluding tax in catalog--> + <magentoCLI command="config:set tax/display/type 3" stepKey="enableShowIncludingExcludingTax"/> + </before> + <after> + <magentoCLI command="config:set tax/display/type 0" stepKey="disableShowIncludingExcludingTax"/> + <!--Delete Products--> + <deleteData createDataKey="simpleProduct" stepKey="deleteProductOne"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProductTwo"/> + <!--Delete Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete Tax Rules--> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule1"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule2"> + <argument name="taxRuleCode" value="{{SimpleTaxRule2.code}}"/> + </actionGroup> + <!--Delete Tax Rates--> + <deleteData createDataKey="createTaxRateCA" stepKey="deleteTaxRate1"/> + <deleteData createDataKey="createTaxRateTX" stepKey="deleteTaxRate2"/> + <!--Delete Customers--> + <deleteData createDataKey="createCustomerCA" stepKey="deleteCustomer1"/> + <deleteData createDataKey="createCustomerTX" stepKey="deleteCustomer2"/> + <!--Logout Admin--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + </after> + + <!-- Login as customer 1--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomer1Login"> + <argument name="Customer" value="$$createCustomerCA$$"/> + </actionGroup> + <!-- Assert Customer Name --> + <actionGroup ref="AssertCustomerWelcomeMessageActionGroup" stepKey="assertCustomerName"> + <argument name="customerFullName" value="$$createCustomerCA.firstname$$ $$createCustomerCA.lastname$$" /> + </actionGroup> + <!-- Navigate to category page --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!-- Assert Product Prices --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct1TaxInclusivePriceCustomer1"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="productPrice" value="$108.25"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct2TaxInclusivePriceCustomer1"> + <argument name="productName" value="$$simpleProduct2.name$$"/> + <argument name="productPrice" value="$216.50"/> + </actionGroup> + <!--Add first product to compare list and cart --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openFirstProductPage"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addFirstProductToCompare"> + <argument name="productVar" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addFirstProductToCart"/> + <!--Add second product to compare list --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openSecondProductPage"> + <argument name="productUrl" value="$$simpleProduct2.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addSecondProductToCompare"> + <argument name="productVar" value="$$simpleProduct2$$"/> + </actionGroup> + <!--Add second product to wishlist --> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addSecondProductToWishlist"> + <argument name="productVar" value="$$simpleProduct2$$"/> + </actionGroup> + <!-- Customer 1 logout --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customer1Logout"/> + <!-- Customer 2 login --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomer2Login"> + <argument name="Customer" value="$$createCustomerTX$$"/> + </actionGroup> + <!-- Assert Wishlist is empty --> + <actionGroup ref="NavigateThroughCustomerTabsActionGroup" stepKey="navigateToWishlist"> + <argument name="navigationItemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmptyActionGroup" stepKey="assertNoItemsInWishlist"/> + <!-- Assert minicart is empty --> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartIsEmpty"/> + <!-- Navigate to category page --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage2"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!-- Assert Compare list is empty --> + <seeElement selector="{{StorefrontComparisonSidebarSection.NoItemsMessage}}" stepKey="assertCompareListIsEmpty"/> + <!-- Assert Product Prices --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct1TaxInclusivePriceCustomer2"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="productPrice" value="$120"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct2TaxInclusivePriceCustomer2"> + <argument name="productName" value="$$simpleProduct2.name$$"/> + <argument name="productPrice" value="$240"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml index 945ebe153a991..e778e3e7067ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml @@ -17,6 +17,7 @@ <useCaseId value="ACP2E-1007"/> <severity value="MAJOR"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create simple products --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml index e5f464920c3ee..1369bcf7fd34f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml @@ -82,7 +82,8 @@ </actionGroup> <!-- Run cron --> - <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCron stepKey="runCron" /> + <magentoCron stepKey="runCronTwice" /> <!-- Check product is present in category after cron run --> <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml index 64901a541a779..cf92289cf8c6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml @@ -15,6 +15,7 @@ <description value="Recently Ordered widget contains no more 5 products if qty of products > 5"/> <testCaseId value="MC-26846"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml index 9c68c08064081..4f9f17ba7e017 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-93973"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml index 59e3700acf5c3..a9fa033ffd4c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-72238"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <!-- Create a category --> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 886b03e0f3c1e..73478b80091f0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -388,8 +388,8 @@ public function initializeDataProvider() return [ [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [], 'linkTypes' => ['related', 'upsell', 'crosssell'], 'expected_links' => [], @@ -423,8 +423,8 @@ public function initializeDataProvider() // Related links [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ @@ -449,8 +449,8 @@ public function initializeDataProvider() // Custom link [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'customlink' => [ 0 => [ @@ -475,8 +475,8 @@ public function initializeDataProvider() // Both links [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ @@ -515,8 +515,8 @@ public function initializeDataProvider() // Undefined link type [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php old mode 100644 new mode 100755 index 974c85b2b5c98..cad43f39f0261 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php @@ -16,6 +16,9 @@ use Magento\Catalog\Controller\Adminhtml\Product\NewAction; use Magento\Catalog\Model\Product; use Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTest; +use Magento\Framework\RegexValidator; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Result\PageFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -42,6 +45,26 @@ class NewActionTest extends ProductTest */ protected $initializationHelper; + /** + * @var RegexValidator|MockObject + */ + private $regexValidator; + + /** + * @var RegexFactory + */ + private $regexValidatorFactoryMock; + + /** + * @var Regex|MockObject + */ + private $regexValidatorMock; + + /** + * @var ForwardFactory&MockObject|MockObject + */ + private $resultForwardFactory; + protected function setUp(): void { $this->productBuilder = $this->createPartialMock( @@ -63,37 +86,78 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultPageFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($this->resultPage); $this->resultForward = $this->getMockBuilder(Forward::class) ->disableOriginalConstructor() ->getMock(); - $resultForwardFactory = $this->getMockBuilder(ForwardFactory::class) + $this->resultForwardFactory = $this->getMockBuilder(ForwardFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + + $this->regexValidatorFactoryMock = $this->getMockBuilder(RegexFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultForwardFactory->expects($this->any()) - ->method('create') - ->willReturn($this->resultForward); + $this->regexValidatorMock = $this->createMock(Regex::class); + $this->regexValidatorFactoryMock->method('create') + ->willReturn($this->regexValidatorMock); + $this->regexValidator = new regexValidator($this->regexValidatorFactoryMock); $this->action = (new ObjectManager($this))->getObject( NewAction::class, [ 'context' => $this->initContext(), 'productBuilder' => $this->productBuilder, 'resultPageFactory' => $resultPageFactory, - 'resultForwardFactory' => $resultForwardFactory, + 'resultForwardFactory' => $this->resultForwardFactory, + 'regexValidator' => $this->regexValidator, ] ); } - public function testExecute() + /** + * Test execute method input validation. + * + * @param string $value + * @param bool $exceptionThrown + * @dataProvider validationCases + */ + public function testExecute(string $value, bool $exceptionThrown): void + { + if ($exceptionThrown) { + $this->action->getRequest()->expects($this->any()) + ->method('getParam') + ->willReturn($value); + $this->resultForwardFactory->expects($this->any()) + ->method('create') + ->willReturn($this->resultForward); + $this->resultForward->expects($this->once()) + ->method('forward') + ->with('noroute') + ->willReturn(true); + $this->assertTrue($this->action->execute()); + } else { + $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn($value); + $this->regexValidatorMock->expects($this->any()) + ->method('isValid') + ->with($value) + ->willReturn(true); + + $this->assertEquals(true, $this->regexValidator->validateParamRegex($value)); + } + } + + /** + * Validation cases. + * + * @return array + */ + public function validationCases(): array { - $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn(true); - $this->action->getRequest()->expects($this->any())->method('getFullActionName') - ->willReturn('catalog_product_new'); - $this->action->execute(); + return [ + 'execute-with-exception' => ['simple\' and true()]|*[self%3a%3ahandle%20or%20self%3a%3alayout',true], + 'execute-without-exception' => ['catalog_product_new',false] + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 4317607fd661e..239e19c84dbf8 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -214,7 +214,7 @@ public function testMoveWhenCannotFindParentCategory(): void { $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('Sorry, but we can\'t find the new parent category you selected.'); - $this->markTestIncomplete('MAGETWO-31165'); + $this->markTestSkipped('MAGETWO-31165'); $parentCategory = $this->createPartialMock( Category::class, ['getId', 'setStoreId', 'load'] @@ -260,7 +260,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory(): void $this->expectExceptionMessage( 'We can\'t move the category because the parent category name matches the child category name.' ); - $this->markTestIncomplete('MAGETWO-31165'); + $this->markTestSkipped('MAGETWO-31165'); $parentCategory = $this->createPartialMock( Category::class, ['getId', 'setStoreId', 'load'] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php deleted file mode 100644 index 0dc0e23ccb3c2..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Plugin; - -use Magento\Catalog\Model\Indexer\Category\Product\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - $processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $processorMock->expects($this->once()) - ->method('markIndexerAsInvalid'); - - $subjectMock = $this->getMockBuilder(Import::class) - ->disableOriginalConstructor() - ->getMock(); - - $import = true; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin\Import($processorMock); - - $this->assertEquals( - $import, - $model->afterImportSource($subjectMock, $import) - ); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php deleted file mode 100644 index 2dafc07ffee15..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Category\Plugin; - -use Magento\Catalog\Model\Indexer\Product\Category\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - $processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $processorMock->expects($this->once()) - ->method('markIndexerAsInvalid'); - - $subjectMock = $this->getMockBuilder(Import::class) - ->disableOriginalConstructor() - ->getMock(); - - $import = true; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin\Import($processorMock); - - $this->assertEquals( - $import, - $model->afterImportSource($subjectMock, $import) - ); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php index 799424f2557c4..0ec5a48e68aab 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product\Attribute\Source\Countryofmanufacture; use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; @@ -46,17 +47,24 @@ class CountryofmanufactureTest extends TestCase */ private $serializerMock; + /** + * @var ResolverInterface + */ + private $localeResolverMock; + protected function setUp(): void { $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); $this->storeMock = $this->createMock(Store::class); $this->cacheConfig = $this->createMock(Config::class); + $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class); $this->objectManagerHelper = new ObjectManager($this); $this->countryOfManufacture = $this->objectManagerHelper->getObject( Countryofmanufacture::class, [ 'storeManager' => $this->storeManagerMock, 'configCacheType' => $this->cacheConfig, + 'localeResolver' => $this->localeResolverMock, ] ); @@ -80,9 +88,10 @@ public function testGetAllOptions($cachedDataSrl, $cachedDataUnsrl) { $this->storeMock->expects($this->once())->method('getCode')->willReturn('store_code'); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); + $this->localeResolverMock->expects($this->once())->method('getLocale')->willReturn('en_US'); $this->cacheConfig->expects($this->once()) ->method('load') - ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code')) + ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code_LOCALE_en_US')) ->willReturn($cachedDataSrl); $this->serializerMock->expects($this->once()) ->method('unserialize') diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php index 305f4acd40d83..1144ce1bbe6c3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Test\Unit\Pricing\Price; +use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\MinimalTierPriceCalculator; use Magento\Catalog\Pricing\Price\TierPrice; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; @@ -73,10 +74,10 @@ private function getValueTierPricesExistShouldReturnMinTierPrice() $notMinPrice = 10; $minAmount = $this->getMockForAbstractClass(AmountInterface::class); - $minAmount->expects($this->once())->method('getValue')->willReturn($minPrice); + $minAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($minPrice); $notMinAmount = $this->getMockForAbstractClass(AmountInterface::class); - $notMinAmount->expects($this->once())->method('getValue')->willReturn($notMinPrice); + $notMinAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($notMinPrice); $tierPriceList = [ [ @@ -89,10 +90,12 @@ private function getValueTierPricesExistShouldReturnMinTierPrice() $this->price->expects($this->once())->method('getTierPriceList')->willReturn($tierPriceList); - $this->priceInfo->expects($this->once())->method('getPrice')->with(TierPrice::PRICE_CODE) - ->willReturn($this->price); + $this->priceInfo->expects($this->atLeastOnce()) + ->method('getPrice') + ->withConsecutive([TierPrice::PRICE_CODE], [FinalPrice::PRICE_CODE]) + ->willReturnOnConsecutiveCalls($this->price, $notMinAmount); - $this->saleable->expects($this->once())->method('getPriceInfo')->willReturn($this->priceInfo); + $this->saleable->expects($this->atLeastOnce())->method('getPriceInfo')->willReturn($this->priceInfo); return $minPrice; } @@ -107,12 +110,8 @@ public function testGetGetAmountMinTierPriceExistShouldReturnAmountObject() $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); $amount = $this->getMockForAbstractClass(AmountInterface::class); + $amount->method('getValue')->willReturn($minPrice); - $this->calculator->expects($this->once()) - ->method('getAmount') - ->with($minPrice, $this->saleable) - ->willReturn($amount); - - $this->assertSame($amount, $this->object->getAmount($this->saleable)); + $this->assertEquals($amount, $this->object->getAmount($this->saleable)); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index f3831e50ef3d9..eadf47601585d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -15,8 +15,11 @@ use Magento\Catalog\Pricing\Render\FinalPriceBox; use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\State; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Event\Test\Unit\ManagerStub; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; @@ -96,11 +99,27 @@ class FinalPriceBoxTest extends TestCase */ private $minimalPriceCalculator; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + /** * @inheritDoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp(): void { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMockForAbstractClass(); + \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); $this->product = $this->getMockBuilder(Product::class) ->addMethods(['getCanShowPrice']) ->onlyMethods(['getPriceInfo', 'isSalable', 'getId']) @@ -183,6 +202,11 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); + $this->minimalPriceCalculator = $this->getMockForAbstractClass(MinimalPriceCalculatorInterface::class); $this->object = $objectManager->getObject( FinalPriceBox::class, @@ -339,10 +363,10 @@ public function testRenderAmountMinimal(): void $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, - 'display_label' => 'As low as', + 'display_label' => __('As low as'), 'price_id' => $priceId, 'include_container' => false, - 'skip_adjustments' => true + 'skip_adjustments' => false ]; $amountRender = $this->createPartialMock(Amount::class, ['toHtml']); @@ -455,6 +479,15 @@ public function testHidePrice(): void */ public function testGetCacheKey(): void { + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(DeploymentConfig::class) + ->willReturn($this->deploymentConfig); + + $this->deploymentConfig->expects($this->any()) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY) + ->willReturn('448198e08af35844a42d3c93c1ef4e03'); $result = $this->object->getCacheKey(); $this->assertStringEndsWith('list-category-page', $result); } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php index 337182abf084c..3f8e6f699d84c 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php @@ -23,16 +23,16 @@ class Price implements ProductRenderCollectorInterface { /** FInal Price key */ - const KEY_FINAL_PRICE = "final_price"; + public const KEY_FINAL_PRICE = "final_price"; /** Minimal Price key */ - const KEY_MINIMAL_PRICE = "minimal_price"; + public const KEY_MINIMAL_PRICE = "minimal_price"; /** Regular Price key */ - const KEY_REGULAR_PRICE = "regular_price"; + public const KEY_REGULAR_PRICE = "regular_price"; /** Max Price key */ - const KEY_MAX_PRICE = "max_price"; + public const KEY_MAX_PRICE = "max_price"; /** * @var PriceCurrencyInterface diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 4421b2991266b..73f8d988bf270 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,7 +31,8 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-aws-s3": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index eeacd0f0970f0..9edd98e24468a 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -291,4 +291,9 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Observer\ImageResizeAfterProductSave"> + <arguments> + <argument name="imageResizeSchedulerFlag" xsi:type="boolean">true</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index e817bcbb42d25..0805d1e48b2d7 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -1181,7 +1181,7 @@ <type name="Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand"> <arguments> <argument name="dimensionSwitchers" xsi:type="array"> - <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher</item> + <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher\Proxy</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index defbf31a6b8f8..81e059adb3bb0 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -819,4 +819,5 @@ Details,Details "Failed to retrieve product links for ""%1""","Failed to retrieve product links for ""%1""" "The linked product SKU is invalid. Verify the data and try again.","The linked product SKU is invalid. Verify the data and try again." "The linked products data is invalid. Verify the data and try again.","The linked products data is invalid. Verify the data and try again." +"The url has invalid characters. Please correct and try again.","The url has invalid characters. Please correct and try again." diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js index d292bd126593c..b4d4ed12d20ba 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js @@ -5,10 +5,9 @@ define([ 'jquery', - 'mageUtils', 'jquery/ui', 'jquery/jstree/jquery.jstree' -], function ($, utils) { +], function ($) { 'use strict'; $.widget('mage.categoryTree', { @@ -87,7 +86,7 @@ define([ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers result = { id: node.id, - text: utils.unescape(node.name) + ' (' + node.product_count + ')', + text: node.name + ' (' + node.product_count + ')', li_attr: { class: node.cls + (!!node.disabled ? ' disabled' : '') //eslint-disable-line no-extra-boolean-cast }, diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml index e56804a06de22..18a2bab2a31bf 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml @@ -6,6 +6,7 @@ ?> <?php +// @codingStandardsIgnoreFile /** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ /** ex: \Magento\Catalog\Pricing\Price\RegularPrice */ @@ -34,7 +35,7 @@ $schema = ($block->getZone() == 'item_view') ? true : false; 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'price_type' => 'oldPrice', 'include_container' => true, - 'skip_adjustments' => true + 'skip_adjustments' => false ]); ?> </span> <?php else :?> diff --git a/app/code/Magento/CatalogCmsGraphQl/README.md b/app/code/Magento/CatalogCmsGraphQl/README.md index f3b36e515ac6e..5e506e4cb6aed 100644 --- a/app/code/Magento/CatalogCmsGraphQl/README.md +++ b/app/code/Magento/CatalogCmsGraphQl/README.md @@ -1,3 +1,3 @@ # CatalogCmsGraphQl -**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules. \ No newline at end of file +**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules. diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index 3c6cc849081ee..fba1f7f8cbcc4 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -18,12 +18,13 @@ use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; /** * Resolver for price_tiers */ -class PriceTiers implements ResolverInterface +class PriceTiers implements ResolverInterface, ResetAfterRequestInterface { /** * @var TiersFactory @@ -185,7 +186,7 @@ private function formatTierPrices(float $productPrice, string $currencyCode, $ti "discount" => $discount, "quantity" => $tierPrice->getQty(), "final_price" => [ - "value" => $tierPrice->getValue(), + "value" => $tierPrice->getValue() * $tierPrice->getQty(), "currency" => $currencyCode ] ]; @@ -216,4 +217,15 @@ private function filterTierPrices( $this->tierPricesQty[$qty] = $key; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->tierPricesQty = []; + $this->formatAndFilterTierPrices = []; + $this->customerGroupId = null; + $this->tiers = null; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php index a1ad456dc5208..954f076295290 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php @@ -13,11 +13,12 @@ use Magento\Customer\Model\GroupManagement; use Magento\Catalog\Api\Data\ProductTierPriceInterface; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Get product tier price information */ -class Tiers +class Tiers implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -173,4 +174,13 @@ private function setProducts(Collection $productCollection): void $this->products[$missingProductId] = null; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->products = []; + $this->filterProductIds = []; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php index bd05e48e23384..3481796323a78 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -16,11 +16,12 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * @inheritdoc */ -class TierPrices implements ResolverInterface +class TierPrices implements ResolverInterface, ResetAfterRequestInterface { /** * @var ValueFactory @@ -94,4 +95,13 @@ function () use ($productId) { } ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupId = null; + $this->tiers = null; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md index 525a1a4f76433..eb1a190e87bc0 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/README.md +++ b/app/code/Magento/CatalogCustomerGraphQl/README.md @@ -1,3 +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 +**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules. diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 09342ceb2f602..9171214a1137a 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -63,6 +63,7 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode 'attribute_id' => 'a.attribute_id', 'attribute_code' => 'a.attribute_code', 'attribute_label' => 'a.frontend_label', + 'attribute_type' => 'a.frontend_input', 'position' => 'attribute_configuration.position' ] ) @@ -137,6 +138,7 @@ private function formatResult(Select $select): array 'attribute_code' => $option['attribute_code'], 'attribute_label' => $option['attribute_store_label'] ? $option['attribute_store_label'] : $option['attribute_label'], + 'attribute_type' => $option['attribute_type'], 'position' => $option['position'], 'options' => [], ]; diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php index e22843573d9d6..5fc50452bb70c 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\CategoryListInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Search\Response\Aggregation; use Magento\Framework\Search\Response\AggregationFactory; use Magento\Framework\Search\Response\BucketFactory; @@ -18,7 +19,7 @@ /** * Class to include only direct subcategories of category in aggregation */ -class IncludeDirectChildrenOnly +class IncludeDirectChildrenOnly implements ResetAfterRequestInterface { /** * @var string @@ -160,4 +161,12 @@ private function filterBucketValues( } return array_values($categoryBucketValues); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->filter = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index afef26aad6046..d1e66613c35f2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -13,6 +13,7 @@ use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; +use Magento\Config\Model\Config\Source\Yesno; /** * @inheritdoc @@ -49,18 +50,26 @@ class Attribute implements LayerBuilderInterface self::CATEGORY_BUCKET ]; + /** + * @var Yesno + */ + private Yesno $YesNo; + /** * @param AttributeOptionProvider $attributeOptionProvider * @param LayerFormatter $layerFormatter + * @param Yesno $YesNo * @param array $bucketNameFilter */ public function __construct( AttributeOptionProvider $attributeOptionProvider, LayerFormatter $layerFormatter, + Yesno $YesNo, $bucketNameFilter = [] ) { $this->attributeOptionProvider = $attributeOptionProvider; $this->layerFormatter = $layerFormatter; + $this->YesNo = $YesNo; $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); } @@ -87,7 +96,11 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array isset($attribute['position']) ? $attribute['position'] : null ); - $options = $this->getSortedOptions($bucket, isset($attribute['options']) ? $attribute['options'] : []); + $options = $this->getSortedOptions( + $bucket, + isset($attribute['options']) ? $attribute['options'] : [], + ($attribute['attribute_type']) ? $attribute['attribute_type']: '' + ); foreach ($options as $option) { $result[$bucketName]['options'][] = $this->layerFormatter->buildItem( $option['label'], @@ -168,9 +181,11 @@ function (AggregationValueInterface $value) { * * @param BucketInterface $bucket * @param array $optionLabels + * @param string $attributeType * @return array + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private function getSortedOptions(BucketInterface $bucket, array $optionLabels): array + private function getSortedOptions(BucketInterface $bucket, array $optionLabels, string $attributeType): array { /** * Option labels array has been sorted @@ -179,7 +194,16 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels): foreach ($bucket->getValues() as $value) { $metrics = $value->getMetrics(); $optionValue = $metrics['value']; - $optionLabel = $optionLabels[$optionValue] ?? $optionValue; + if (isset($optionLabels[$optionValue])) { + $optionLabel = $optionLabels[$optionValue]; + } else { + if ($attributeType === 'boolean') { + $yesNoOptions = $this->YesNo->toArray(); + $optionLabel = $yesNoOptions[$optionValue]; + } else { + $optionLabel = $optionValue; + } + } $options[$optionValue] = $metrics + ['label' => $optionLabel]; } @@ -188,7 +212,7 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels): */ foreach ($options as $optionId => $option) { if (!is_array($options[$optionId])) { - unset($options[$optionId]); + unset($options[$optionId]); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php index b8689cc8868d7..c65c0872d0873 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -18,13 +18,14 @@ use Magento\Framework\Api\Search\BucketInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Category layer builder * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Category implements LayerBuilderInterface +class Category implements LayerBuilderInterface, ResetAfterRequestInterface { /** * @var string @@ -201,4 +202,17 @@ private function getStoreCategoryIds(int $storeId): array ); return $collection->getAllIds(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$bucketMap = [ + self::CATEGORY_BUCKET => [ + 'request_name' => 'category_uid', + 'label' => 'Category' + ], + ]; + } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php index 6df29fa256923..d3b4e31366525 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php @@ -24,7 +24,7 @@ class LayerFormatter public function buildLayer($layerName, $itemsCount, $requestName, $position = null): array { return [ - 'label' => $layerName, + 'label' => __($layerName), 'count' => $itemsCount, 'attribute_code' => $requestName, 'position' => isset($position) ? (int)$position : null diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index d67a50875b81d..70520c31a7724 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -98,7 +98,7 @@ public function __construct( public function build(array $args, bool $includeAggregation): SearchCriteriaInterface { $searchCriteria = $this->builder->build('products', $args); - $isSearch = !empty($args['search']); + $isSearch = isset($args['search']); $this->updateRangeFilters($searchCriteria); if ($includeAggregation) { $attributeData = $this->eavConfig->getAttribute(Product::ENTITY, 'price'); @@ -122,7 +122,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } $this->addEntityIdSort($searchCriteria); - $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); + $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter']['category_id'])); $searchCriteria->setCurrentPage($args['currentPage']); $searchCriteria->setPageSize($args['pageSize']); diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 34f5dd831686c..63a998022df80 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -10,13 +10,15 @@ use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Joins attributes for provided field node field names. */ -class AttributesJoiner +class AttributesJoiner implements ResetAfterRequestInterface { /** * @var array @@ -61,39 +63,56 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection, Resol * * @param FieldNode $fieldNode * @param ResolveInfo $resolveInfo + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return string[] */ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; - $selectedFields = []; - $fragmentFields = []; /** @var FieldNode $field */ - foreach ($query as $field) { - if ($field->kind === NodeKind::INLINE_FRAGMENT) { - $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); - } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && - ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { - - foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { - if (isset($spreadNode->selectionSet->selections)) { - $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); - } else { + $result = $this->getQueryData($query, $resolveInfo); + if ($result['fragmentFields']) { + $result['selectedFields'] = array_merge([], $result['selectedFields'], ...$result['fragmentFields']); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($result['selectedFields'])); + } + return $this->getFieldNodeSelections($fieldNode); + } + + /** + * Get an array of queried data. + * + * @param NodeList $query + * @param ResolveInfo $resolveInfo + * @return array + */ + public function getQueryData(NodeList $query, ResolveInfo $resolveInfo): array + { + $selectedFields = $fragmentFields = $data = []; + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + if ($spreadNode->kind === NodeKind::FIELD && isset($spreadNode->name)) { $selectedFields[] = $spreadNode->name->value; } + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; } - } else { - $selectedFields[] = $field->name->value; } + } else { + $selectedFields[] = $field->name->value; } - if ($fragmentFields) { - $selectedFields = array_merge([], $selectedFields, ...$fragmentFields); - } - $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } + $data['selectedFields'] = $selectedFields; + $data['fragmentFields'] = $fragmentFields; - return $this->getFieldNodeSelections($fieldNode); + return $data; } /** @@ -111,15 +130,22 @@ private function addInlineFragmentFields( ): array { $query = $inlineFragmentField->selectionSet->selections; /** @var FieldNode $field */ + $fragmentFields = []; foreach ($query as $field) { if ($field->kind === NodeKind::INLINE_FRAGMENT) { $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); } elseif (isset($field->selectionSet->selections)) { - continue; + if ($field->kind === NodeKind::FIELD && isset($field->name)) { + $inlineFragmentFields[] = $field->name->value; + } + $fragmentFields[] = $this->getQueryFields($field, $resolveInfo); } else { $inlineFragmentFields[] = $field->name->value; } } + if ($fragmentFields) { + $inlineFragmentFields = array_merge([], $inlineFragmentFields, ...$fragmentFields); + } return array_unique($inlineFragmentFields); } @@ -172,4 +198,12 @@ private function setSelectionsForFieldNode(FieldNode $fieldNode, array $selected { $this->queryFields[$fieldNode->name->value][$fieldNode->name->loc->start] = $selectedFields; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->queryFields = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index 356ff17183a57..2bd1714537a0a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -11,6 +11,7 @@ use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\SelectionNode; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -66,13 +67,13 @@ private function calculateRecursive(ResolveInfo $resolveInfo, Node $node) : int * Add inline fragment fields into calculating of category depth * * @param ResolveInfo $resolveInfo - * @param InlineFragmentNode $inlineFragmentField + * @param SelectionNode $inlineFragmentField * @param array $depth * @return int */ private function addInlineFragmentDepth( ResolveInfo $resolveInfo, - InlineFragmentNode $inlineFragmentField, + SelectionNode $inlineFragmentField, $depth = [] ): int { $selections = $inlineFragmentField->selectionSet->selections; @@ -80,7 +81,7 @@ private function addInlineFragmentDepth( foreach ($selections as $field) { if ($field->kind === NodeKind::INLINE_FRAGMENT) { $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); - } elseif ($field->selectionSet && $field->selectionSet->selections) { + } elseif (!empty($field->selectionSet) && $field->selectionSet->selections) { $depth[] = $this->calculate($resolveInfo, $field); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php index 215b28be0579c..2f16e9ccb318f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -6,9 +6,11 @@ namespace Magento\CatalogGraphQl\Model\Config; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributesCollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Config\ReaderInterface; use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; -use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection; /** * Adds custom/eav attribute to catalog products sorting in the GraphQL config. @@ -31,20 +33,24 @@ class SortAttributeReader implements ReaderInterface private $mapper; /** - * @var AttributesCollection + * @var AttributesCollectionFactory */ - private $attributesCollection; + private $attributesCollectionFactory; /** * @param MapperInterface $mapper - * @param AttributesCollection $attributesCollection + * @param AttributesCollection $attributesCollection @deprecated @see $attributesCollectionFactory + * @param AttributesCollectionFactory|null $attributesCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( MapperInterface $mapper, - AttributesCollection $attributesCollection + AttributesCollection $attributesCollection, + ?AttributesCollectionFactory $attributesCollectionFactory = null ) { $this->mapper = $mapper; - $this->attributesCollection = $attributesCollection; + $this->attributesCollectionFactory = $attributesCollectionFactory + ?? ObjectManager::getInstance()->get(AttributesCollectionFactory::class); } /** @@ -58,7 +64,8 @@ public function read($scope = null) : array { $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE); $config =[]; - $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); + $attributes = $this->attributesCollectionFactory->create() + ->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ foreach ($attributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); @@ -73,7 +80,6 @@ public function read($scope = null) : array ]; } } - return $config; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php new file mode 100644 index 0000000000000..7930c597adeae --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Output; + +use Magento\Catalog\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Format attributes metadata for GraphQL output + */ +class AttributeMetadata implements GetAttributeDataInterface +{ + /** + * @var string + */ + private string $entityType; + + /** + * @param string $entityType + */ + public function __construct(string $entityType) + { + $this->entityType = $entityType; + } + + /** + * Retrieve formatted attribute data + * + * @param Attribute $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws LocalizedException + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + if ($entityType !== $this->entityType) { + return []; + } + + $metadata = [ + 'is_comparable' => $attribute->getIsComparable() === "1", + 'is_filterable' => $attribute->getIsFilterable() === "1", + 'is_filterable_in_search' => $attribute->getIsFilterableInSearch() === "1", + 'is_searchable' => $attribute->getIsSearchable() === "1", + 'is_html_allowed_on_front' => $attribute->getIsHtmlAllowedOnFront() === "1", + 'is_used_for_price_rules' => $attribute->getIsUsedForPriceRules() === "1", + 'is_used_for_promo_rules' => $attribute->getIsUsedForPromoRules() === "1", + 'is_visible_in_advanced_search' => $attribute->getIsVisibleInAdvancedSearch() === "1", + 'is_visible_on_front' => $attribute->getIsVisibleOnFront() === "1", + 'is_wysiwyg_enabled' => $attribute->getIsWysiwygEnabled() === "1", + 'used_in_product_listing' => $attribute->getUsedInProductListing() === "1", + 'apply_to' => null + ]; + + if (!empty($attribute->getApplyTo())) { + $metadata['apply_to'] = array_map('strtoupper', $attribute->getApplyTo()); + } + + if (!empty($attribute->getAdditionalData())) { + $additionalData = json_decode($attribute->getAdditionalData(), true); + $metadata = array_merge( + $metadata, + array_map('strtoupper', $additionalData) + ); + } + + return $metadata; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index d7118d71db89b..68b051f39c3a2 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -8,7 +8,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; @@ -27,46 +26,39 @@ class Categories implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $collection; - - /** - * Accumulated category ids - * - * @var array - */ - private $categoryIds = []; + private CollectionFactory $collectionFactory; /** * @var AttributesJoiner */ - private $attributesJoiner; + private AttributesJoiner $attributesJoiner; /** * @var CustomAttributesFlattener */ - private $customAttributesFlattener; + private CustomAttributesFlattener $customAttributesFlattener; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var CategoryHydrator */ - private $categoryHydrator; + private CategoryHydrator $categoryHydrator; /** * @var ProductCategories */ - private $productCategories; + private ProductCategories $productCategories; /** * @var StoreManagerInterface */ - private $storeManager; + private StoreManagerInterface $storeManager; /** * @param CollectionFactory $collectionFactory @@ -86,7 +78,7 @@ public function __construct( ProductCategories $productCategories, StoreManagerInterface $storeManager ) { - $this->collection = $collectionFactory->create(); + $this->collectionFactory = $collectionFactory; $this->attributesJoiner = $attributesJoiner; $this->customAttributesFlattener = $customAttributesFlattener; $this->valueFactory = $valueFactory; @@ -105,43 +97,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; $storeId = $this->storeManager->getStore()->getId(); $categoryIds = $this->productCategories->getCategoryIdsByProduct((int)$product->getId(), (int)$storeId); - $this->categoryIds = array_merge($this->categoryIds, $categoryIds); - $that = $this; - + $collection = $this->collectionFactory->create(); return $this->valueFactory->create( - function () use ($that, $categoryIds, $info) { + function () use ($categoryIds, $info, $collection) { $categories = []; - if (empty($that->categoryIds)) { + if (empty($categoryIds)) { return []; } - - if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); - $this->collection->addIdFilter($this->categoryIds); + if (!$collection->isLoaded()) { + $this->attributesJoiner->join($info->fieldNodes[0], $collection, $info); + $collection->addIdFilter($categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ - foreach ($this->collection as $item) { + foreach ($collection as $item) { if (in_array($item->getId(), $categoryIds)) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); + $requestedFields = $this->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { continue; } - // If not all requested fields were extracted from the collection, start more complex extraction $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item); } } - return $categories; } ); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php index df725c02eb5bd..1a52916a85c01 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php @@ -9,12 +9,14 @@ use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Exception\LocalizedException; /** * @inheritdoc @@ -22,31 +24,35 @@ class Product implements ResolverInterface { /** - * @var ProductDataProvider + * @var ProductDataProviderFactory */ - private $productDataProvider; + private ProductDataProviderFactory $productDataProviderFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var ProductFieldsSelector */ - private $productFieldsSelector; + private ProductFieldsSelector $productFieldsSelector; /** - * @param ProductDataProvider $productDataProvider + * @param ProductDataProvider $productDataProvider Deprecated. Use $productDataProviderFactory * @param ValueFactory $valueFactory * @param ProductFieldsSelector $productFieldsSelector + * @param ProductDataProviderFactory|null $productDataProviderFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ProductDataProvider $productDataProvider, ValueFactory $valueFactory, - ProductFieldsSelector $productFieldsSelector + ProductFieldsSelector $productFieldsSelector, + ProductDataProviderFactory $productDataProviderFactory = null ) { - $this->productDataProvider = $productDataProvider; + $this->productDataProviderFactory = $productDataProviderFactory + ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class); $this->valueFactory = $valueFactory; $this->productFieldsSelector = $productFieldsSelector; } @@ -59,12 +65,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['sku'])) { throw new GraphQlInputException(__('No child sku found for product link.')); } - $this->productDataProvider->addProductSku($value['sku']); + $productDataProvider = $this->productDataProviderFactory->create(); + $productDataProvider->addProductSku($value['sku']); $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info); - $this->productDataProvider->addEavAttributes($fields); - - $result = function () use ($value, $context) { - $data = $value['product'] ?? $this->productDataProvider->getProductBySku($value['sku'], $context); + $productDataProvider->addEavAttributes($fields); + $result = function () use ($value, $context, $productDataProvider) { + $data = $value['product'] ?? $productDataProvider->getProductBySku($value['sku'], $context); if (empty($data)) { return null; } @@ -75,7 +81,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var \Magento\Catalog\Model\Product $productModel */ $data = $productModel->getData(); $data['model'] = $productModel; - if (!empty($productModel->getCustomAttributes())) { foreach ($productModel->getCustomAttributes() as $customAttribute) { if (!isset($data[$customAttribute->getAttributeCode()])) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php index 359d295095667..79dd1450125ed 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php @@ -14,11 +14,12 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Returns media url */ -class Url implements ResolverInterface +class Url implements ResolverInterface, ResetAfterRequestInterface { /** * @var ImageFactory @@ -100,4 +101,12 @@ private function getImageUrl(string $imageType, ?string $imagePath): string return $image->getUrl(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->placeholderCache = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index 25db5207af285..938f6c359b060 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -8,8 +8,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; use Magento\CatalogGraphQl\Model\PriceRangeDataProvider; -use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; -use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Config\Element\Field; @@ -20,33 +18,17 @@ */ class PriceRange implements ResolverInterface { - /** - * @var Discount - */ - private Discount $discount; - - /** - * @var PriceProviderPool - */ - private PriceProviderPool $priceProviderPool; - /** * @var PriceRangeDataProvider */ private PriceRangeDataProvider $priceRangeDataProvider; /** - * @param PriceProviderPool $priceProviderPool - * @param Discount $discount * @param PriceRangeDataProvider|null $priceRangeDataProvider */ public function __construct( - PriceProviderPool $priceProviderPool, - Discount $discount, PriceRangeDataProvider $priceRangeDataProvider = null ) { - $this->priceProviderPool = $priceProviderPool; - $this->discount = $discount; $this->priceRangeDataProvider = $priceRangeDataProvider ?? ObjectManager::getInstance()->get(PriceRangeDataProvider::class); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php new file mode 100644 index 0000000000000..367724891026a --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.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\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\FilterProductCustomAttribute; +use Magento\Catalog\Model\Product; +use Magento\CatalogGraphQl\Model\ProductDataProvider; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\EavGraphQl\Model\Resolver\GetFilteredAttributes; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * + * Format a product's custom attribute information to conform to GraphQL schema representation + */ +class ProductCustomAttributes implements ResolverInterface +{ + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + + /** + * @var ProductDataProvider + */ + private ProductDataProvider $productDataProvider; + + /** + * @var GetFilteredAttributes + */ + private GetFilteredAttributes $getFilteredAttributes; + + /** + * @var FilterProductCustomAttribute + */ + private FilterProductCustomAttribute $filterCustomAttribute; + + /** + * @param GetAttributeValueInterface $getAttributeValue + * @param ProductDataProvider $productDataProvider + * @param GetFilteredAttributes $getFilteredAttributes + * @param FilterProductCustomAttribute $filterCustomAttribute + */ + public function __construct( + GetAttributeValueInterface $getAttributeValue, + ProductDataProvider $productDataProvider, + GetFilteredAttributes $getFilteredAttributes, + FilterProductCustomAttribute $filterCustomAttribute + ) { + $this->getAttributeValue = $getAttributeValue; + $this->productDataProvider = $productDataProvider; + $this->getFilteredAttributes = $getFilteredAttributes; + $this->filterCustomAttribute = $filterCustomAttribute; + } + + /** + * @inheritdoc + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return array + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $filtersArgs = $args['filters'] ?? []; + + $productCustomAttributes = $this->getFilteredAttributes->execute( + $filtersArgs, + ProductAttributeInterface::ENTITY_TYPE_CODE + ); + + $attributeCodes = array_map( + function (AttributeInterface $customAttribute) { + return $customAttribute->getAttributeCode(); + }, + $productCustomAttributes['items'] + ); + + $filteredAttributeCodes = $this->filterCustomAttribute->execute(array_flip($attributeCodes)); + + /** @var Product $product */ + $product = $value['model']; + $productData = $this->productDataProvider->getProductDataById((int)$product->getId()); + + $customAttributes = []; + foreach ($filteredAttributeCodes as $attributeCode => $value) { + if (!array_key_exists($attributeCode, $productData)) { + continue; + } + $attributeValue = $productData[$attributeCode]; + if (is_array($attributeValue)) { + $attributeValue = implode(',', $attributeValue); + } + $customAttributes[] = [ + 'attribute_code' => $attributeCode, + 'value' => $attributeValue + ]; + } + + return [ + 'items' => array_map( + function (array $customAttribute) { + return $this->getAttributeValue->execute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $customAttribute['attribute_code'], + $customAttribute['value'] + ); + }, + $customAttributes + ), + 'errors' => $productCustomAttributes['errors'] + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 3139c35774008..ab9fed035cc35 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,7 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; -use GraphQL\Language\AST\NodeKind; +use Magento\CatalogGraphQl\Model\AttributesJoiner; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -19,14 +19,23 @@ class ProductFieldsSelector /** * @var FieldTranslator */ - private $fieldTranslator; + private FieldTranslator $fieldTranslator; + + /** + * @var AttributesJoiner + */ + private AttributesJoiner $attributesJoiner; /** * @param FieldTranslator $fieldTranslator + * @param AttributesJoiner $attributesJoiner */ - public function __construct(FieldTranslator $fieldTranslator) - { + public function __construct( + FieldTranslator $fieldTranslator, + AttributesJoiner $attributesJoiner + ) { $this->fieldTranslator = $fieldTranslator; + $this->attributesJoiner = $attributesJoiner; } /** @@ -36,27 +45,17 @@ public function __construct(FieldTranslator $fieldTranslator) * @param string $productNodeName * @return string[] */ - public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product') : array + public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product'): array { $fieldNames = []; foreach ($info->fieldNodes as $node) { if ($node->name->value !== $productNodeName) { continue; } - foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { - foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); - } + $queryFields = $this->attributesJoiner->getQueryFields($node, $info); + $fieldNames[] = $queryFields; } - return $fieldNames; + return array_merge(...$fieldNames); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php index 3091cffb619c2..d5afac28354bf 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php @@ -9,13 +9,14 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ResourceModel\Website\Collection as WebsiteCollection; use Magento\Store\Model\ResourceModel\Website\CollectionFactory as WebsiteCollectionFactory; /** * Collection to fetch websites data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var WebsiteCollection @@ -133,4 +134,13 @@ private function fetch() : array } return $this->websites; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productIds = []; + $this->websites = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php index ab0531ad09513..63cd205fd87f9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php @@ -11,11 +11,12 @@ use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; use Magento\Eav\Model\Attribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Gather all eav and custom attributes to use in a GraphQL schema for products */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var AttributeCollectionFactory @@ -95,4 +96,12 @@ public function getRequestAttributes(array $fieldNames) : array return $matchedAttributes; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->collection = null; + } } 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 a528efcb4a81a..d3e30dd48f280 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 @@ -10,12 +10,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\GraphQl\Model\Query\ContextInterface; /** * Deferred resolver for product data. */ -class Product +class Product implements ResetAfterRequestInterface { /** * @var ProductDataProvider @@ -144,4 +145,14 @@ private function fetch(ContextInterface $context = null): void $this->productList[$product->getSku()] = ['model' => $product]; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productList = []; + $this->productSkus = []; + $this->attributeCodes = []; + } } 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 30be41072242b..3e955ae303453 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/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php new file mode 100644 index 0000000000000..9941c5a9cbb4a --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; + +/** + * Category Path processor class for category url path argument + */ +class CategoryUrlPathArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'category_id'; + + private const UID = 'category_uid'; + + private const URL_PATH = 'category_url_path'; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param CollectionFactory $collectionFactory + */ + public function __construct(CollectionFactory $collectionFactory) + { + $this->collectionFactory = $collectionFactory; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $idFilter = $args['filter'][self::ID] ?? []; + $uidFilter = $args['filter'][self::UID] ?? []; + $pathFilter = $args['filter'][self::URL_PATH] ?? []; + + if (!empty($pathFilter) && $fieldName === 'products') { + if (!empty($idFilter)) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::URL_PATH]) + ); + } elseif (!empty($uidFilter)) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::UID, self::URL_PATH]) + ); + } + + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + $collection->addAttributeToSelect('entity_id'); + $collection->addAttributeToFilter('url_path', $pathFilter); + + if ($collection->count() === 0) { + throw new GraphQlInputException( + __('No category with the provided `%1` was found', [self::URL_PATH]) + ); + } elseif ($collection->count() === 1) { + $category = $collection->getFirstItem(); + $args['filter'][self::ID]['eq'] = $category->getId(); + } else { + $categoryIds = []; + foreach ($collection as $category) { + $categoryIds[] = $category->getId(); + } + $args['filter'][self::ID]['in'] = $categoryIds; + } + + unset($args['filter'][self::URL_PATH]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 2c612da4f433a..c4d189cd7cb0c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -186,7 +186,8 @@ private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCrit { $productFields = (array)$info->getFieldSelection(1); $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); - $processedArgs = $this->argsSelection->process((string) $info->fieldName, $args); + $fieldName = $info->fieldName ?? ""; + $processedArgs = $this->argsSelection->process((string) $fieldName, $args); $searchCriteria = $this->searchCriteriaBuilder->build($processedArgs, $includeAggregations); return $searchCriteria; diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php index 59970335d3d10..b406c053cd207 100644 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php @@ -97,50 +97,60 @@ public function testBuild(): void $filter = $this->createMock(Filter::class); $searchCriteria = $this->getMockBuilder(SearchCriteriaInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $attributeInterface = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $attributeInterface->setData(['is_filterable' => 0]); $this->builder->expects($this->any()) - ->method('build') - ->with('products', $args) - ->willReturn($searchCriteria); + ->method('build') + ->with('products', $args) + ->willReturn($searchCriteria); $searchCriteria->expects($this->any())->method('getFilterGroups')->willReturn([]); $this->eavConfig->expects($this->any()) - ->method('getAttribute') - ->with(Product::ENTITY, 'price') - ->willReturn($attributeInterface); - - $this->sortOrderBuilder->expects($this->once()) - ->method('setField') - ->with('_id') - ->willReturnSelf(); - $this->sortOrderBuilder->expects($this->once()) - ->method('setDirection') - ->with('DESC') - ->willReturnSelf(); - $this->sortOrderBuilder->expects($this->any()) - ->method('create') - ->willReturn([]); - - $this->filterBuilder->expects($this->once()) - ->method('setField') - ->with('visibility') - ->willReturnSelf(); - $this->filterBuilder->expects($this->once()) - ->method('setValue') - ->with("") - ->willReturnSelf(); - $this->filterBuilder->expects($this->once()) - ->method('setConditionType') - ->with('in') - ->willReturnSelf(); - - $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter); + ->method('getAttribute') + ->with(Product::ENTITY, 'price') + ->willReturn($attributeInterface); + $sortOrderList = ['relevance', '_id']; + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('setField') + ->withConsecutive([$sortOrderList[0]], [$sortOrderList[1]]) + ->willReturnSelf(); + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('setDirection') + ->with('DESC') + ->willReturnSelf(); + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('create') + ->willReturn([]); + + $filterOrderList = ['search_term', 'visibility']; + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setField') + ->withConsecutive([$filterOrderList[0]], [$filterOrderList[1]]) + ->willReturnSelf(); + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setValue') + ->with('') + ->willReturnSelf(); + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setConditionType') + ->withConsecutive([''], ['in']) + ->willReturnSelf(); + + $this->filterBuilder + ->expects($this->exactly(2)) + ->method('create') + ->willReturn($filter); $this->filterGroupBuilder->expects($this->any()) ->method('addFilter') diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php new file mode 100644 index 0000000000000..489742db45f79 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Category; + +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\SelectionSetNode; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DepthCalculatorTest extends TestCase +{ + /** + * @var DepthCalculator + */ + private DepthCalculator $depthCalculator; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var FieldNode|MockObject + */ + private $fieldNodeMock; + + /** + * Await for depth of '1' if selectionSet is null + * @return void + */ + public function testCalculateWithNullAsSelectionSet(): void + { + $this->fieldNodeMock->kind = NodeKind::FIELD; + /** @var SelectionSetNode $selectionSetNode */ + $selectionSetNode = new SelectionSetNode([]); + $selectionSetNode->selections = $this->getSelectionsArrayForNullCase(); + $this->fieldNodeMock->selectionSet = $selectionSetNode; + $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock); + $this->assertSame(1, $result); + } + + /** + * Await for depth of '2' if selectionSet is not null + * @return void + */ + public function testCalculateNonNullAsSelectionSet(): void + { + $this->fieldNodeMock->kind = NodeKind::FIELD; + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = $this->getSelectionsArrayForNonNullCase(); + $this->fieldNodeMock->selectionSet = $selectionSetNode; + $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock); + $this->assertEquals(2, $result); + } + + /** + * @return NodeList + */ + private function getSelectionsArrayForNullCase() + { + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = $this->getNodeList(); + $inlineFragmentNode = $this->getNewInlineFragmentNode(); + $inlineFragmentNode->selectionSet = $selectionSetNode; + return new NodeList([ + $this->getNewFieldNode(), + $inlineFragmentNode + ]); + } + + /** + * @return FieldNode + */ + private function getNewFieldNode() + { + return new FieldNode([]); + } + + /** + * @return InlineFragmentNode + */ + private function getNewInlineFragmentNode() + { + return new InlineFragmentNode([]); + } + + /** + * @return NodeList + */ + private function getSelectionsArrayForNonNullCase() + { + $newFieldNode = $this->getNewFieldNode(); + $newFieldNode->selectionSet = $this->getSelectionSetNode(); + $newFieldNode->selectionSet->selections = $this->getNodeList(); + $newFieldNode->selectionSet->selections[] = $this->getNewFieldNode(); + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = new NodeList([$newFieldNode]); + $inlineFragmentNode = $this->getNewInlineFragmentNode(); + $inlineFragmentNode->selectionSet = $selectionSetNode; + return new NodeList([ + $newFieldNode, + $inlineFragmentNode + ]); + } + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->depthCalculator = new DepthCalculator(); + $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + $this->fieldNodeMock = $this->getMockBuilder(FieldNode::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return \GraphQL\Language\AST\SelectionSetNode + */ + protected function getSelectionSetNode($nodes = []): SelectionSetNode + { + return new SelectionSetNode($nodes); + } + + /** + * @return \GraphQL\Language\AST\NodeList + */ + protected function getNodeList(): NodeList + { + return new NodeList([]); + } +} diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index fbc4172226c58..f51d069ec0684 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -14,6 +14,7 @@ "magento/module-catalog-search": "*", "magento/framework": "*", "magento/module-graph-ql": "*", + "magento/module-config": "*", "magento/module-advanced-search": "*" }, "suggest": { diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index de72c4a559c4d..692dd5679182c 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -78,6 +78,7 @@ <type name="Magento\Framework\GraphQl\Query\Resolver\ArgumentsCompositeProcessor"> <arguments> <argument name="processors" xsi:type="array"> + <item name="category_url_path" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUrlPathArgsProcessor</item> <item name="category_uid" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor</item> <item name="category_uids" xsi:type="object">Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor</item> <item name="parent_category_uids" xsi:type="object">Magento\CatalogGraphQl\Model\Category\ParentCategoryUidsArgsProcessor</item> diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index d66ee50ba03a5..9fc1a47594458 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -208,4 +208,40 @@ </argument> </arguments> </virtualType> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeMetadata"> + <arguments> + <argument name="entityTypes" xsi:type="array"> + <item name="CATALOG_PRODUCT" xsi:type="string">CatalogAttributeMetadata</item> + <item name="CATALOG_CATEGORY" xsi:type="string">CatalogAttributeMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeEntityTypeEnum" xsi:type="array"> + <item name="catalog_product" xsi:type="string">catalog_product</item> + <item name="catalog_category" xsi:type="string">catalog_category</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="catalog_product" xsi:type="object">GetCatalogProductAttributesMetadata</item> + <item name="catalog_category" xsi:type="object">GetCatalogCategoryAttributesMetadata</item> + </argument> + </arguments> + </type> + <virtualType name="GetCatalogProductAttributesMetadata" type="Magento\CatalogGraphQl\Model\Output\AttributeMetadata"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="GetCatalogCategoryAttributesMetadata" type="Magento\CatalogGraphQl\Model\Output\AttributeMetadata"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/module.xml b/app/code/Magento/CatalogGraphQl/etc/module.xml index 87696c129a714..1837648d1ca4d 100644 --- a/app/code/Magento/CatalogGraphQl/etc/module.xml +++ b/app/code/Magento/CatalogGraphQl/etc/module.xml @@ -13,6 +13,7 @@ <module name="Magento_Store"/> <module name="Magento_Eav"/> <module name="Magento_GraphQl"/> + <module name="Magento_Config"/> <module name="Magento_StoreGraphQl"/> <module name="Magento_EavGraphQl"/> </sequence> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 0b42655ed73cc..3d3875bb5c588 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -125,6 +125,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ 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") canonical_url: String @doc(description: "The relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") media_gallery: [MediaGalleryInterface] @doc(description: "An array of media gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") + custom_attributesV2(filters: AttributeFilterInput): ProductCustomAttributes @doc(description: "Product custom attributes.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductCustomAttributes") } interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains attributes specific to tangible products.") { @@ -344,6 +345,7 @@ type CategoryProducts @doc(description: "Contains details about the products ass input ProductAttributeFilterInput @doc(description: "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.") { category_id: FilterEqualTypeInput @deprecated(reason: "Use `category_uid` instead.") @doc(description: "Deprecated: use `category_uid` to filter product by category ID.") category_uid: FilterEqualTypeInput @doc(description: "Filter product by the unique ID for a `CategoryInterface` object.") + category_url_path: FilterEqualTypeInput @doc(description: "Filter product by category URL path.") } input CategoryFilterInput @doc(description: "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.") @@ -531,3 +533,38 @@ type SimpleWishlistItem implements WishlistItemInterface @doc(description: "Cont type VirtualWishlistItem implements WishlistItemInterface @doc(description: "Contains a virtual product wish list item.") { } + +enum AttributeEntityTypeEnum { + CATALOG_PRODUCT + CATALOG_CATEGORY +} + +type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Catalog attribute metadata.") { + apply_to: [CatalogAttributeApplyToEnum] @doc(description: "To which catalog types an attribute can be applied.") + is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.") + is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.") + is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.") + is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.") + is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.") + is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.") + is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.") + is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.") + is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.") + is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.") + used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.") +} + +enum CatalogAttributeApplyToEnum { + SIMPLE + VIRTUAL + BUNDLE + DOWNLOADABLE + CONFIGURABLE + GROUPED + CATEGORY +} + +type ProductCustomAttributes @doc(description: "Product custom attributes") { + items: [AttributeValueInterface!]! @doc(description: "Requested custom attributes") + errors: [AttributeMetadataError!]! @doc(description: "Errors when retrieving custom attributes metadata.") +} diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 2c0d7f45af19a..2784530566bb0 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -640,10 +640,13 @@ protected function prepareCatalogInventory(array $productIds) if ($stockItemRow['use_config_max_sale_qty']) { $stockItemRow['max_sale_qty'] = $this->stockConfiguration->getMaxSaleQty(); } - if ($stockItemRow['use_config_min_sale_qty']) { $stockItemRow['min_sale_qty'] = $this->stockConfiguration->getMinSaleQty(); } + if ($stockItemRow['use_config_manage_stock']) { + $stockItemRow['manage_stock'] = $this->stockConfiguration->getManageStock(); + } + $stockItemRows[$productId] = $stockItemRow; } return $stockItemRows; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 3058c6f6619d4..a0ee8d77d69f3 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -8,6 +8,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as ProductPriceIndexer; use Magento\Catalog\Model\Product\Visibility; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; @@ -16,6 +18,7 @@ use Magento\CatalogImportExport\Model\Import\Product\Skip; use Magento\CatalogImportExport\Model\Import\Product\StatusProcessor; use Magento\CatalogImportExport\Model\Import\Product\StockProcessor; +use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\CatalogImportExport\Model\StockItemImporterInterface; use Magento\CatalogImportExport\Model\StockItemProcessorInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; @@ -48,6 +51,7 @@ */ class Product extends AbstractEntity { + private const COL_NAME_FORMAT = '/[\x00-\x1F\x7F]/'; private const DEFAULT_GLOBAL_MULTIPLE_VALUE_SEPARATOR = ','; public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; @@ -461,7 +465,7 @@ class Product extends AbstractEntity /** * Array of supported product types as keys with appropriate model object as value. * - * @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType[] + * @var AbstractType[] */ protected $_productTypeModels = []; @@ -1102,6 +1106,7 @@ protected function _deleteProducts() 'catalog_product_import_bunch_delete_after', ['adapter' => $this, 'bunch' => $bunch] ); + $this->reindexProducts($idsToDelete); } } return $this; @@ -1222,6 +1227,11 @@ private function initImagesArrayKeys() */ protected function _initTypeModels() { + // When multiple imports are processed in a single php process, + // these memory caches may interfere with the import result. + AbstractType::$commonAttributesCache = []; + AbstractType::$invAttributesCache = []; + AbstractType::$attributeCodeToId = []; $productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode()); $fieldsMap = []; $specialAttributes = []; @@ -1233,11 +1243,11 @@ protected function _initTypeModels() __('Entity type model \'%1\' is not found', $productTypeConfig['model']) ); } - if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { + if (!$model instanceof AbstractType) { throw new LocalizedException( __( 'Entity type model must be an instance of ' - . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class + . AbstractType::class ) ); } @@ -1624,6 +1634,10 @@ protected function _saveProducts() // the bunch of products will pass for the event with url_key column. $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; } + if (!empty($rowData[self::COL_NAME])) { + // remove null byte character + $rowData[self::COL_NAME] = preg_replace(self::COL_NAME_FORMAT, '', $rowData[self::COL_NAME]); + } $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1660,7 +1674,7 @@ protected function _saveProducts() $prevAttributeSet, $attributes ); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (Skip $skip) { // Product is skipped. Go on to the next one. } @@ -2034,10 +2048,7 @@ private function saveProductAttributesPhase( } if (self::SCOPE_STORE == $rowScope) { if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) { - // check website defaults already set - if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) { - $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); - } + $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) { $storeIds = [$rowStore]; } @@ -2466,9 +2477,17 @@ private function reindexStockStatus(array $productIds): void */ private function reindexProducts($productIdsToReindex = []) { - $indexer = $this->indexerRegistry->get('catalog_product_category'); - if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) { - $indexer->reindexList($productIdsToReindex); + if (is_array($productIdsToReindex) && !empty($productIdsToReindex)) { + $indexersToReindex = [ + ProductCategoryIndexer::INDEXER_ID, + ProductPriceIndexer::INDEXER_ID + ]; + foreach ($indexersToReindex as $id) { + $indexer = $this->indexerRegistry->get($id); + if (!$indexer->isScheduled()) { + $indexer->reindexList($productIdsToReindex); + } + } } } @@ -2678,7 +2697,7 @@ public function validateRow(array $rowData, $rowNum) // set attribute set code into row data for followed attribute validation in type model $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code']; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType $productTypeValidator */ + /** @var AbstractType $productTypeValidator */ // isRowValid can add error to general errors pull if row is invalid $productTypeValidator = $this->_productTypeModels[$newSku['type_id']]; $productTypeValidator->isRowValid( diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index 3a6d8d2533e85..856a985014ff7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product; +use Magento\Store\Model\Store; + /** * @api * @since 100.0.2 @@ -119,6 +121,7 @@ protected function createCategory($name, $parentId) $category->setIsActive(true); $category->setIncludeInMenu(true); $category->setAttributeSetId($category->getDefaultAttributeSetId()); + $category->setStoreId(Store::DEFAULT_STORE_ID); $category->save(); $this->categoriesCache[$category->getId()] = $category; return $category->getId(); diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php deleted file mode 100644 index d8a926f7cfe31..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin; - -class Import -{ - /** - * @var \Magento\Catalog\Model\Indexer\Category\Product\Processor - */ - protected $_indexerCategoryProductProcessor; - - /** - * @param \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor - */ - public function __construct( - \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor - ) { - $this->_indexerCategoryProductProcessor = $indexerCategoryProductProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param boolean $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_indexerCategoryProductProcessor->isIndexerScheduled()) { - $this->_indexerCategoryProductProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php deleted file mode 100644 index 0d0d4ea80530a..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin; - -class Import -{ - /** - * @var \Magento\Catalog\Model\Indexer\Product\Category\Processor - */ - protected $_indexerProductCategoryProcessor; - - /** - * @param \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor - */ - public function __construct( - \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor - ) { - $this->_indexerProductCategoryProcessor = $indexerProductCategoryProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param boolean $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_indexerProductCategoryProcessor->isIndexerScheduled()) { - $this->_indexerProductCategoryProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php deleted file mode 100644 index 87020be7cd30d..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin; - -class Import -{ - /** - * @var \Magento\Framework\Indexer\IndexerRegistry - */ - private $indexerRegistry; - - /** - * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry - */ - public function __construct(\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry) - { - $this->indexerRegistry = $indexerRegistry; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $result) - { - $priceIndexer = $this->indexerRegistry->get(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID); - if (!$priceIndexer->isScheduled()) { - $priceIndexer->invalidate(); - } - return $result; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php deleted file mode 100644 index c83045b2062cb..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogImportExport\Model\Indexer\Stock\Plugin; - -class Import -{ - /** - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor - */ - protected $_stockndexerProcessor; - - /** - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockndexerProcessor - */ - public function __construct(\Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockndexerProcessor) - { - $this->_stockndexerProcessor = $stockndexerProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param Object $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_stockndexerProcessor->isIndexerScheduled()) { - $this->_stockndexerProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php index c2ce4c6499ecc..9e3a2f220f73d 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php @@ -13,16 +13,17 @@ use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class CategoryProcessorTest extends TestCase { - const PARENT_CATEGORY_ID = 1; + public const PARENT_CATEGORY_ID = 1; - const CHILD_CATEGORY_ID = 2; + public const CHILD_CATEGORY_ID = 2; - const CHILD_CATEGORY_NAME = 'Child'; + public const CHILD_CATEGORY_NAME = 'Child'; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager @@ -48,7 +49,7 @@ class CategoryProcessorTest extends TestCase private $childCategory; /** - * \Magento\Catalog\Model\Category + * @var \Magento\Catalog\Model\Category */ private $parentCategory; @@ -200,4 +201,19 @@ protected function setPropertyValue(&$object, $property, $value) $reflectionProperty->setValue($object, $value); return $object; } + + /** + * @throws \ReflectionException + */ + public function testCategoriesCreatedForGlobalScope() + { + $this->childCategory->expects($this->once()) + ->method('setStoreId') + ->with(Store::DEFAULT_STORE_ID); + + $reflection = new \ReflectionClass($this->categoryProcessor); + $createCategoryReflection = $reflection->getMethod('createCategory'); + $createCategoryReflection->setAccessible(true); + $createCategoryReflection->invokeArgs($this->categoryProcessor, ['testCategory', 2]); + } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php deleted file mode 100644 index d5ae17d5c392f..0000000000000 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogImportExport\Test\Unit\Model\Indexer\Product\Price\Plugin; - -use Magento\Catalog\Model\Indexer\Product\Price\Processor; -use Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin\Import; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Indexer\Model\Indexer; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - /** - * @var ObjectManager - */ - protected $_objectManager; - - /** - * @var Import - */ - protected $_model; - - /** - * @var Indexer|MockObject - */ - protected $_indexerMock; - - /** - * @var IndexerRegistry|MockObject - */ - protected $indexerRegistryMock; - - protected function setUp(): void - { - $this->_objectManager = new ObjectManager($this); - - $this->_indexerMock = $this->getMockBuilder(Indexer::class) - ->addMethods(['getPriceIndexer']) - ->onlyMethods(['getId', 'invalidate', 'isScheduled']) - ->disableOriginalConstructor() - ->getMock(); - $this->indexerRegistryMock = $this->createPartialMock( - IndexerRegistry::class, - ['get'] - ); - - $this->_model = $this->_objectManager->getObject( - Import::class, - ['indexerRegistry' => $this->indexerRegistryMock] - ); - } - - /** - * Test AfterImportSource() - */ - public function testAfterImportSource() - { - $this->_indexerMock->expects($this->once())->method('invalidate'); - $this->indexerRegistryMock->expects($this->any()) - ->method('get') - ->with(Processor::INDEXER_ID) - ->willReturn($this->_indexerMock); - $this->_indexerMock->expects($this->any()) - ->method('isScheduled') - ->willReturn(false); - - $importMock = $this->createMock(\Magento\ImportExport\Model\Import::class); - $this->assertEquals('return_value', $this->_model->afterImportSource($importMock, 'return_value')); - } -} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php deleted file mode 100644 index 3659cde191b54..0000000000000 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogImportExport\Test\Unit\Model\Indexer\Stock\Plugin; - -use Magento\CatalogInventory\Model\Indexer\Stock\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor| - * \PHPUnit\Framework\MockObject\MockObject $processorMock - */ - $processorMock = $this->createPartialMock( - Processor::class, - ['markIndexerAsInvalid', 'isIndexerScheduled'] - ); - - $subjectMock = $this->createMock(Import::class); - $processorMock->expects($this->any())->method('markIndexerAsInvalid'); - $processorMock->expects($this->any())->method('isIndexerScheduled')->willReturn(false); - - $someData = [1, 2, 3]; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Stock\Plugin\Import($processorMock); - $this->assertEquals($someData, $model->afterImportSource($subjectMock, $someData)); - } -} diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml index 43fdda6227ac7..4150fca46fa6a 100644 --- a/app/code/Magento/CatalogImportExport/etc/di.xml +++ b/app/code/Magento/CatalogImportExport/etc/di.xml @@ -12,11 +12,7 @@ <preference for="Magento\CatalogImportExport\Model\Export\ProductFilterInterface" type="Magento\CatalogImportExport\Model\Export\ProductFilters" /> <type name="Magento\ImportExport\Model\Import"> <plugin name="catalogProductFlatIndexerImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Flat\Plugin\Import" /> - <plugin name="invalidatePriceIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin\Import" /> - <plugin name="invalidateStockIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Stock\Plugin\Import" /> <plugin name="invalidateEavIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Eav\Plugin\Import" /> - <plugin name="invalidateProductCategoryIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin\Import" /> - <plugin name="invalidateCategoryProductIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin\Import" /> </type> <type name="Magento\CatalogImportExport\Model\Import\Product\StockProcessor"> <arguments> diff --git a/app/code/Magento/CatalogImportExport/etc/import.xml b/app/code/Magento/CatalogImportExport/etc/import.xml index 522b478752f01..05f8ceb5425d4 100644 --- a/app/code/Magento/CatalogImportExport/etc/import.xml +++ b/app/code/Magento/CatalogImportExport/etc/import.xml @@ -9,7 +9,5 @@ <entity name="catalog_product" label="Products" model="Magento\CatalogImportExport\Model\Import\Product" behaviorModel="Magento\ImportExport\Model\Source\Import\Behavior\Basic" /> <entityType entity="catalog_product" name="simple" model="Magento\CatalogImportExport\Model\Import\Product\Type\Simple" /> <entityType entity="catalog_product" name="virtual" model="Magento\CatalogImportExport\Model\Import\Product\Type\Virtual" /> - <relatedIndexer entity="catalog_product" name="catalog_product_price" /> - <relatedIndexer entity="catalog_product" name="catalogsearch_fulltext" /> <relatedIndexer entity="catalog_product" name="catalog_product_flat" /> </config> diff --git a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php index 96bf5bd965355..1ee8e1a97e89f 100644 --- a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php @@ -8,13 +8,14 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Store\Model\Store; /** * MinSaleQty value manipulation helper */ -class Minsaleqty +class Minsaleqty implements ResetAfterRequestInterface { /** * Core store config @@ -61,6 +62,14 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->minSaleQtyCache = []; + } + /** * Retrieve fixed qty value * diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php index 8b0849c8874bc..9df634c225d71 100644 --- a/app/code/Magento/CatalogInventory/Model/Configuration.php +++ b/app/code/Magento/CatalogInventory/Model/Configuration.php @@ -9,98 +9,96 @@ use Magento\CatalogInventory\Helper\Minsaleqty as MinsaleqtyHelper; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Catalog\Model\ProductTypes\ConfigInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; -/** - * Class Configuration - */ -class Configuration implements StockConfigurationInterface +class Configuration implements StockConfigurationInterface, ResetAfterRequestInterface { /** * Default website id */ - const DEFAULT_WEBSITE_ID = 1; + public const DEFAULT_WEBSITE_ID = 1; /** * Inventory options config path */ - const XML_PATH_GLOBAL = 'cataloginventory/options/'; + public const XML_PATH_GLOBAL = 'cataloginventory/options/'; /** * Subtract config path */ - const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract'; + public const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract'; /** * Back in stock config path */ - const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock'; + public const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock'; /** * Item options config path */ - const XML_PATH_ITEM = 'cataloginventory/item_options/'; + public const XML_PATH_ITEM = 'cataloginventory/item_options/'; /** * Max qty config path */ - const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty'; + public const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty'; /** * Min sale qty config path */ - const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty'; + public const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty'; /** * Max sale qty config path */ - const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty'; + public const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty'; /** * Back orders config path */ - const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders'; + public const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders'; /** * Notify stock config path */ - const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty'; + public const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty'; /** * Manage stock config path */ - const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock'; + public const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock'; /** * Enable qty increments config path */ - const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments'; + public const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments'; /** * Qty increments config path */ - const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments'; + public const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments'; /** * Show out of stock config path */ - const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock'; + public const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock'; /** * Auto return config path */ - const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return'; + public const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return'; /** * Path to configuration option 'Display product stock status' */ - const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status'; + public const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status'; /** * Threshold qty config path */ - const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty'; + public const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty'; /** * @var ConfigInterface @@ -122,7 +120,7 @@ class Configuration implements StockConfigurationInterface /** * All product types registry in scope of quantity availability * - * @var array + * @var array|null */ protected $isQtyTypeIds; @@ -151,6 +149,14 @@ public function __construct( $this->storeManager = $storeManager; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isQtyTypeIds = null; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index c871a8dee65f4..c5b71d9345804 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -90,7 +90,7 @@ public function clean(array $productIds, callable $reindex) $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); $categoryIds = $this->getCategoryIdsByProductIds($productIds); - if ($categoryIds){ + if ($categoryIds) { $this->cacheContext->registerEntities(Category::CACHE_TAG, array_unique($categoryIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); } @@ -162,7 +162,7 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array } } - return $productIds; + return array_map('intval', $productIds); } /** @@ -176,7 +176,7 @@ private function getCategoryIdsByProductIds(array $productIds): array $categoryProductTable = $this->resource->getTableName('catalog_category_product'); $select = $this->getConnection()->select() ->from(['catalog_category_product' => $categoryProductTable], ['category_id']) - ->where('product_id IN (?)', $productIds); + ->where('product_id IN (?)', $productIds, \Zend_Db::INT_TYPE); return $this->getConnection()->fetchCol($select); } diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php index f104552b4e0fc..ea4c35de053b2 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php @@ -35,6 +35,8 @@ class StockItem /** * @var StockStateProviderInterface + * @deprecated + * @see was overriding ItemBackorders value with the Default Scope value; caused discrepancy in multistock config */ private $stockStateProvider; @@ -122,11 +124,6 @@ public function initialize( $quoteItem->setHasError(true); } - /* We need to ensure that any possible plugin will not erase the data */ - $backOrdersQty = $this->stockStateProvider->checkQuoteItemQty($stockItem, $rowQty, $qtyForCheck, $qty) - ->getItemBackorders(); - $result->setItemBackorders($backOrdersQty); - if ($stockItem->hasIsChildItem()) { $stockItem->unsIsChildItem(); } diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php index 600bf9897a036..363f91916fb75 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php @@ -33,7 +33,7 @@ class QuoteItemQtyList public function getQty($productId, $quoteItemId, $quoteId, $itemQty) { $qty = $itemQty; - if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && !in_array( + if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && $quoteItemId !== null && !in_array( $quoteItemId, $this->_checkedQuoteItems[$quoteId][$productId]['items'] ) diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php index 8238c1e8f6b21..50e16ad5ed04a 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php @@ -8,11 +8,9 @@ use Magento\CatalogInventory\Api\Data\StockInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -/** - * Class StockRegistryStorage - */ -class StockRegistryStorage +class StockRegistryStorage implements ResetAfterRequestInterface { /** * @var array @@ -30,6 +28,8 @@ class StockRegistryStorage private $stockStatuses = []; /** + * Get Stock Data + * * @param int $scopeId * @return StockInterface */ @@ -39,6 +39,8 @@ public function getStock($scopeId) } /** + * Set Stock cache + * * @param int $scopeId * @param StockInterface $value * @return void @@ -49,6 +51,8 @@ public function setStock($scopeId, StockInterface $value) } /** + * Delete cached Stock based on scopeId + * * @param int|null $scopeId * @return void */ @@ -62,6 +66,8 @@ public function removeStock($scopeId = null) } /** + * Retrieve Stock Item + * * @param int $productId * @param int $scopeId * @return StockItemInterface @@ -72,6 +78,8 @@ public function getStockItem($productId, $scopeId) } /** + * Update Stock Item + * * @param int $productId * @param int $scopeId * @param StockItemInterface $value @@ -83,6 +91,8 @@ public function setStockItem($productId, $scopeId, StockItemInterface $value) } /** + * Remove stock Item based on productId & scopeId + * * @param int $productId * @param int|null $scopeId * @return void @@ -97,6 +107,8 @@ public function removeStockItem($productId, $scopeId = null) } /** + * Retrieve stock status + * * @param int $productId * @param int $scopeId * @return StockStatusInterface @@ -107,6 +119,8 @@ public function getStockStatus($productId, $scopeId) } /** + * Update stock Status + * * @param int $productId * @param int $scopeId * @param StockStatusInterface $value @@ -118,6 +132,8 @@ public function setStockStatus($productId, $scopeId, StockStatusInterface $value } /** + * Clear stock status + * * @param int $productId * @param int|null $scopeId * @return void @@ -142,4 +158,12 @@ public function clean() $this->stocks = []; $this->stockStatuses = []; } + + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->clean(); + } } diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml index 6f388c3e6c6d1..9a198dd571def 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -36,5 +36,7 @@ <element name="maxiQtyAllowedInCartError" type="text" selector="[name='product[stock_data][max_sale_qty]'] + label.admin__field-error"/> <element name="backorders" type="select" selector="//*[@name='product[stock_data][backorders]']"/> <element name="useConfigSettingsForBackorders" type="checkbox" selector="//input[@name='product[stock_data][use_config_backorders]']"/> + <element name="checkConfigSettingsAdvancedInventory" type="checkbox" selector="//input[@name='product[stock_data][{{args}}]']/..//label[text()='Use Config Settings']/..//input[@type='checkbox']" parameterized="true"/> + <element name="selectManageStockOption" type="select" selector="//select[@name='product[stock_data][manage_stock]']"/> </section> </sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml index 26dd08be0a8c6..75eb122118ef9 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml @@ -19,6 +19,7 @@ <testCaseId value="MC-17636"/> <group value="catalog"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml index cd1931cf7fb78..60e903f59225c 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94135"/> <group value="CatalogInventory"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml index 951ca2b0ee80b..0242673fb0034 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml @@ -16,6 +16,7 @@ <description value="Placing the order for out of stock products and zero quantity"/> <severity value="CRITICAL"/> <testCaseId value="AC-5262"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml index e17c8fe65d4cf..c7beaa3c6835d 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-38242"/> <testCaseId value="MC-38883"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 794f5d92da1e8..22dce1a600601 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php @@ -122,9 +122,6 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto $this->selectMock->expects($this->any()) ->method('from') ->willReturnSelf(); - $this->selectMock->expects($this->any()) - ->method('where') - ->willReturnSelf(); $this->selectMock->expects($this->any()) ->method('joinLeft') ->willReturnSelf(); @@ -141,6 +138,21 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter], ] ); + $this->connectionMock->expects($this->exactly(3)) + ->method('select') + ->willReturn($this->selectMock); + $this->selectMock->expects($this->exactly(7)) + ->method('where') + ->withConsecutive( + ['product_id IN (?)'], + ['stock_id = ?'], + ['website_id = ?'], + ['product_id IN (?)'], + ['stock_id = ?'], + ['website_id = ?'], + ['product_id IN (?)', [123], \Zend_Db::INT_TYPE] + ) + ->willReturnSelf(); $this->connectionMock->expects($this->exactly(1)) ->method('fetchCol') ->willReturn([$categoryId]); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php index 24f46c2414f35..9591b84b4c8d5 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php @@ -173,10 +173,7 @@ public function testInitializeWithSubitem() ->method('checkQuoteItemQty') ->withAnyParameters() ->willReturn($result); - $this->stockStateProviderMock->expects($this->once()) - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->willReturn($result); + $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty'); $product->expects($this->once()) ->method('getCustomOption') ->with('product_type') @@ -213,7 +210,7 @@ public function testInitializeWithSubitem() $quoteItem->expects($this->once())->method('setUseOldQty')->with('item')->willReturnSelf(); $result->expects($this->exactly(2))->method('getMessage')->willReturn('message'); $quoteItem->expects($this->once())->method('setMessage')->with('message')->willReturnSelf(); - $result->expects($this->exactly(3))->method('getItemBackorders')->willReturn('backorders'); + $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn('backorders'); $quoteItem->expects($this->once())->method('setBackorders')->with('backorders')->willReturnSelf(); $quoteItem->expects($this->once())->method('setStockStateResult')->with($result)->willReturnSelf(); @@ -276,10 +273,7 @@ public function testInitializeWithoutSubitem() ->method('checkQuoteItemQty') ->withAnyParameters() ->willReturn($result); - $this->stockStateProviderMock->expects($this->once()) - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->willReturn($result); + $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty'); $product->expects($this->once()) ->method('getCustomOption') ->with('product_type') @@ -299,7 +293,7 @@ public function testInitializeWithoutSubitem() $result->expects($this->once())->method('getHasQtyOptionUpdate')->willReturn(false); $result->expects($this->once())->method('getItemUseOldQty')->willReturn(null); $result->expects($this->once())->method('getMessage')->willReturn(null); - $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn(null); + $result->expects($this->exactly(1))->method('getItemBackorders')->willReturn(null); $this->model->initialize($stockItem, $quoteItem, $qty); } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php index 44ce1fe6a3451..df9d3ee94dbbd 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php @@ -49,6 +49,10 @@ public function testSingleQuoteItemQty() $qty = $this->quoteItemQtyList->getQty(125, 1, 11232, 1); $this->assertEquals($this->itemQtyTestValue, $qty); + + $this->itemQtyTestValue = 2; + $qty = $this->quoteItemQtyList->getQty(125, null, 11232, 1); + $this->assertNotEquals($this->itemQtyTestValue, $qty); } /** diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index e19757401621a..5d1f91f962834 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; @@ -19,6 +21,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; @@ -168,6 +171,11 @@ class IndexBuilder */ private $productLoader; + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + /** * @var ProductCollectionFactory */ @@ -195,6 +203,7 @@ class IndexBuilder * @param TableSwapper|null $tableSwapper * @param TimezoneInterface|null $localeDate * @param ProductCollectionFactory|null $productCollectionFactory + * @param IndexerRegistry|null $indexerRegistry * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -219,7 +228,8 @@ public function __construct( ProductLoader $productLoader = null, TableSwapper $tableSwapper = null, TimezoneInterface $localeDate = null, - ProductCollectionFactory $productCollectionFactory = null + ProductCollectionFactory $productCollectionFactory = null, + IndexerRegistry $indexerRegistry = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -261,6 +271,8 @@ public function __construct( ObjectManager::getInstance()->get(TableSwapper::class); $this->localeDate = $localeDate ?? ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->indexerRegistry = $indexerRegistry ?? + ObjectManager::getInstance()->get(IndexerRegistry::class); $this->productCollectionFactory = $productCollectionFactory ?? ObjectManager::getInstance()->get(ProductCollectionFactory::class); } @@ -333,6 +345,15 @@ protected function doReindexByIds($ids) $this->reindexRuleProductPrice->execute($this->batchCount, $productId); } + //the case was not handled via indexer dependency decorator or via mview configuration + $ruleIndexer = $this->indexerRegistry->get(RuleProductProcessor::INDEXER_ID); + if ($ruleIndexer->isScheduled()) { + $priceIndexer = $this->indexerRegistry->get(PriceIndexProcessor::INDEXER_ID); + if (!$priceIndexer->isScheduled()) { + $priceIndexer->reindexList($ids); + } + } + $this->reindexRuleGroupWebsite->execute(); } diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 82b3fd228002e..1eca8469db1c6 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -31,26 +31,28 @@ use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Model\ResourceModel\Iterator; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Rule\Model\AbstractModel; use Magento\Store\Model\StoreManagerInterface; /** * Catalog Rule data model * - * @method \Magento\CatalogRule\Model\Rule setFromDate(string $value) - * @method \Magento\CatalogRule\Model\Rule setToDate(string $value) - * @method \Magento\CatalogRule\Model\Rule setCustomerGroupIds(string $value) + * @method Rule setFromDate(string $value) + * @method Rule setToDate(string $value) + * @method Rule setCustomerGroupIds(string $value) * @method string getWebsiteIds() - * @method \Magento\CatalogRule\Model\Rule setWebsiteIds(string $value) + * @method Rule setWebsiteIds(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ -class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, IdentityInterface +class Rule extends AbstractModel implements RuleInterface, IdentityInterface, ResetAfterRequestInterface { /** * Prefix of model events names @@ -405,9 +407,16 @@ public function callbackValidateProduct($args) $product->setData($args['row']); $websites = $this->_getWebsitesMap(); + $websiteIds = $this->getWebsiteIds(); + if (!is_array($websiteIds)) { + $websiteIds = explode(',', $websiteIds); + } $results = []; foreach ($websites as $websiteId => $defaultStoreId) { + if (!in_array($websiteId, $websiteIds)) { + continue; + } $product->setStoreId($defaultStoreId); $results[$websiteId] = $this->getConditions()->validate($product); } @@ -910,4 +919,12 @@ public function clearPriceRulesData(): void { self::$_priceRulesData = []; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_priceRulesData = []; + } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml index 724664917fecc..aded63210e3fc 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-25351"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="createDropdownAttribute"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml index d3a349fb3a19c..ac57c49c74993 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27565"/> <group value="catalogRule"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml index ee32fa1901f43..114c579033919 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml @@ -15,6 +15,7 @@ <description value="Admin should be able to apply the catalog price rule for fixed bundle product with custom options"/> <severity value="MAJOR"/> <testCaseId value="AC-4027"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index ce8d2dd1507fb..d8a35834cb929 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-71"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml index 2be55819a1004..4e8830176dc65 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26702"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml index 77228dde8797f..c80b2b56fc35c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml @@ -15,6 +15,7 @@ <description value="Admin can not create catalog price rule with the invalid data"/> <severity value="MAJOR"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml index 1d4b21cb04a60..b97191a37c656 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-13977"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml index c1d98a7e7128e..8a354a29c1ee2 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index ba446380a4f63..b3e4b88e93366 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14772"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index c127f19db3749..1e0aa54d545ae 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -18,6 +18,7 @@ <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index a616a7ab172f1..022b7a7aec274 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -18,6 +18,7 @@ <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml index c3078a052116a..298ce68731e1d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml @@ -18,6 +18,7 @@ <useCaseId value="ACP2E-1206"/> <group value="catalogRule"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 703b3655480ce..c13c85d34792a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-79"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php index 45c9db38c5dd3..c8a4c7a59e344 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php @@ -207,15 +207,15 @@ public function testCallbackValidateProduct($validate): void 'updated_at' => '2014-06-25 14:37:15' ]; $this->storeManager->expects($this->any())->method('getWebsites')->with(false) - ->willReturn([$this->websiteModel, $this->websiteModel]); + ->willReturn([$this->websiteModel, $this->websiteModel, $this->websiteModel]); $this->websiteModel ->method('getId') - ->willReturnOnConsecutiveCalls('1', '2'); + ->willReturnOnConsecutiveCalls('1', '2', '3'); $this->websiteModel->expects($this->any())->method('getDefaultStore') ->willReturn($this->storeModel); $this->storeModel ->method('getId') - ->willReturnOnConsecutiveCalls('1', '2'); + ->willReturnOnConsecutiveCalls('1', '2', '3'); $this->combineFactory->expects($this->any())->method('create') ->willReturn($this->condition); $this->condition->expects($this->any())->method('validate') @@ -224,12 +224,14 @@ public function testCallbackValidateProduct($validate): void $this->productModel->expects($this->any())->method('getId') ->willReturn(1); + $this->rule->setWebsiteIds('1,2'); $this->rule->callbackValidateProduct($args); $matchingProducts = $this->rule->getMatchingProductIds(); foreach ($matchingProducts['1'] as $matchingRules) { $this->assertEquals($validate, $matchingRules); } + $this->assertNull($matchingProducts['1']['3'] ?? null); } /** diff --git a/app/code/Magento/CatalogRuleGraphQl/README.md b/app/code/Magento/CatalogRuleGraphQl/README.md index 6f9761fedecbb..13a8f4a62e963 100644 --- a/app/code/Magento/CatalogRuleGraphQl/README.md +++ b/app/code/Magento/CatalogRuleGraphQl/README.md @@ -1,3 +1,3 @@ # CatalogRuleGraphQl -The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests. \ No newline at end of file +The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests. diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php index f014c6d133187..1904367ec3d66 100644 --- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php @@ -21,11 +21,9 @@ class DataProvider implements DataProviderInterface /** * Autocomplete limit */ - const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit'; + public const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit'; /** - * Query factory - * * @var QueryFactory */ protected $queryFactory; @@ -38,8 +36,6 @@ class DataProvider implements DataProviderInterface protected $itemFactory; /** - * Limit - * * @var int */ protected $limit; @@ -68,8 +64,12 @@ public function __construct( */ public function getItems() { - $collection = $this->getSuggestCollection(); $query = $this->queryFactory->get()->getQueryText(); + if (!$query) { + return []; + } + + $collection = $this->getSuggestCollection(); $result = []; foreach ($collection as $item) { $resultItem = $this->itemFactory->create([ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 9b66606d37a9e..d29928306f24b 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -219,10 +219,23 @@ public function __construct( ->get(DefaultFilterStrategyApplyChecker::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->queryText = null; + $this->search = null; + $this->searchCriteriaBuilder = null; + $this->searchResult = null; + $this->filterBuilder = null; + $this->searchOrders = null; + } + /** * Get search. * - * @deprecated 100.1.0 * @return \Magento\Search\Api\SearchInterface */ private function getSearch() @@ -237,6 +250,7 @@ private function getSearch() * Test search. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Search\Api\SearchInterface $object * @return void * @since 100.1.0 @@ -249,7 +263,6 @@ public function setSearch(\Magento\Search\Api\SearchInterface $object) /** * Set search criteria builder. * - * @deprecated 100.1.0 * @return \Magento\Framework\Api\Search\SearchCriteriaBuilder */ private function getSearchCriteriaBuilder() @@ -265,6 +278,7 @@ private function getSearchCriteriaBuilder() * Set search criteria builder. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object * @return void * @since 100.1.0 @@ -277,7 +291,6 @@ public function setSearchCriteriaBuilder(\Magento\Framework\Api\Search\SearchCri /** * Get filter builder. * - * @deprecated 100.1.0 * @return \Magento\Framework\Api\FilterBuilder */ private function getFilterBuilder() @@ -292,6 +305,7 @@ private function getFilterBuilder() * Set filter builder. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Framework\Api\FilterBuilder $object * @return void * @since 100.1.0 diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index 7e9be408a3850..10e72e0155ff3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -23,22 +23,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection private $indexUsageEnforcements; /** - * Attribute collection - * * @var array */ protected $_attributesCollection; /** - * Search query - * * @var string */ protected $_searchQuery; /** - * Attribute collection factory - * * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory */ protected $_attributeCollectionFactory; @@ -119,6 +113,16 @@ public function __construct( $this->indexUsageEnforcements = $indexUsageEnforcements; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_attributesCollection = null; + $this->_searchQuery = null; + } + /** * Add search query filter * @@ -240,6 +244,8 @@ private function isIndexExists(string $table, string $index) : bool * @param mixed $query * @param bool $searchOnlyInCurrentStore Search only in current store or in all stores * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = true) { diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml index 72358cd002f44..c3374d4b6967a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml @@ -18,6 +18,7 @@ <group value="Search"/> <testCaseId value="MC-37809"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml index 6361c076ce177..1163f90989eb0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml index c376456a64ac4..a0d3d60dc5a7e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml index 3719899d39ec6..dd429fcec645c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml index 96b9714a343ca..972ecd669a63e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index 911ed45b82f7d..8006ea9d34c7a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value="cataloginventory_stock catalog_product_price"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 9312eeb1c1070..6e21c8f213756 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index 02e8e30f37782..99e90ed63a417 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index 7b0835302cbdf..e6d32b5e9cc78 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml index 120f2fff76333..cdfaa88b998f1 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14789"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml index fd77b1f6b4aec..dc299476bee40 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14790"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml index 5a487e3f0fd41..d8fd296cf7252 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14786"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml index c4461eb31039a..b75cb4d2ca225 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14788"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml index b52cd9fc43882..f1e14d9f58db8 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14784"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml index 6f21f79145a30..df35689ba655b 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14785"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml index ad817a03c2e22..11c03dbb22e06 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml @@ -18,6 +18,7 @@ <group value="CatalogSearch"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml index b2b6bbb473091..b85c92c2a708b 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14795"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="productWith130CharName" stepKey="createSimpleProduct"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml index 62f4e3da1059c..6b573ac37a2e7 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14791"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <!-- Overwrite search to use name --> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml index 7f21972ce801b..3da958b341307 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14792"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="productWithSpecialCharacters" stepKey="createSimpleProduct"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml index 1e777ab0ab66b..dae971667184c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14783"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml index b2b4ef9cc4782..db6c10c7e45f0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml index 45cec0a899361..af72b38ebb0fe 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml index 33dff8aefa334..4a24b0aa83d49 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="ABC_123_SimpleProduct" stepKey="createProduct2" after="createProduct"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml index c4622d02a5152..1130368ba50a3 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <remove keyForRemoval="createProduct"/> <remove keyForRemoval="deleteProduct"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml index ca5e237099681..c70cd5802aa8a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc_dfj"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml index 7508830e0f050..05b19a53c6b74 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-12421"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml index e1f297b6dffed..174a6f2985468 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml @@ -18,6 +18,7 @@ <group value="searchFrontend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!-- 1. Navigate to Frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml index cceac0475aa78..e7940a50bb809 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml @@ -14,6 +14,7 @@ <title value="Unable negative price use to advanced search"/> <description value="Check unable negative price use to advanced search by price from and price to"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml index d54090576128f..11deec4a95d6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-69181"/> <group value="catalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create the category --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml index cfff1d1b3bdc1..66f3ba291f408 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="search"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php index 18d18352b8d89..5282212cacb90 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php @@ -147,4 +147,17 @@ private function buildCollection(array $data) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionData)); } + + public function testGetItemsWithEmptyQueryText() + { + $this->query->expects($this->once()) + ->method('getQueryText') + ->willReturn(''); + $this->query->expects($this->never()) + ->method('getSuggestCollection'); + $this->itemFactory->expects($this->never()) + ->method('create'); + $result = $this->model->getItems(); + $this->assertEmpty($result); + } } diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml index 376e4ced4d5ac..9a84bf4c458d5 100644 --- a/app/code/Magento/CatalogSearch/etc/search_request.xml +++ b/app/code/Magento/CatalogSearch/etc/search_request.xml @@ -67,7 +67,7 @@ <queries> <query xsi:type="boolQuery" name="advanced_search_container" boost="1"> <queryReference clause="should" ref="sku_query"/> - <queryReference clause="should" ref="price_query"/> + <queryReference clause="must" ref="price_query"/> <queryReference clause="should" ref="category_query"/> <queryReference clause="must" ref="visibility_query"/> </query> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php index 569de155c6e3a..6b6f68d0bdc5d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php @@ -6,8 +6,9 @@ namespace Magento\CatalogUrlRewrite\Model\Category; use Magento\Catalog\Model\Category; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -class ChildrenCategoriesProvider +class ChildrenCategoriesProvider implements ResetAfterRequestInterface { /** * @var array @@ -15,6 +16,8 @@ class ChildrenCategoriesProvider protected $childrenIds = []; /** + * Get Children Categories + * * @param \Magento\Catalog\Model\Category $category * @param boolean $recursive * @return \Magento\Catalog\Model\Category[] @@ -29,6 +32,8 @@ public function getChildren(Category $category, $recursive = false) } /** + * Retrieve category children ids + * * @param \Magento\Catalog\Model\Category $category * @param boolean $recursive * @return int[] @@ -50,4 +55,12 @@ public function getChildrenIds(Category $category, $recursive = false) } return $this->childrenIds[$cacheKey]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->childrenIds = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php index cb9f3fecb4bca..3948e1ca3f180 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php @@ -8,11 +8,12 @@ use Magento\Catalog\Model\ResourceModel\CategoryFactory; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for category ids and its subcategories ids */ -class DataCategoryHashMap implements HashMapInterface +class DataCategoryHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -57,7 +58,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -86,10 +87,18 @@ private function getAllCategoryChildrenIds(CategoryInterface $category) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php index a70f533fbe5ba..9aa560f2aa3f5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php @@ -6,11 +6,12 @@ namespace Magento\CatalogUrlRewrite\Model\Map; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for categories used by products found in root category */ -class DataCategoryUsedInProductsHashMap implements HashMapInterface +class DataCategoryUsedInProductsHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -40,8 +41,7 @@ public function __construct( } /** - * Returns an array of product ids for all DataProductHashMap list, - * that occur in other categories not part of DataCategoryHashMap list + * Returns product ids for all DataProductHashMap list from other categories not part of DataCategoryHashMap list * * @param int $categoryId * @return array @@ -81,7 +81,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -93,7 +93,7 @@ public function getData($categoryId, $key) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { @@ -101,4 +101,12 @@ public function resetData($categoryId) $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php index 39e4c1f0f2012..44f183b5de8b7 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php @@ -7,11 +7,12 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for products ids from a category and subcategories */ -class DataProductHashMap implements HashMapInterface +class DataProductHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -81,7 +82,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -93,11 +94,19 @@ public function getData($categoryId, $key) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php index fbacddac1ce02..d3c032a5c26aa 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php @@ -9,12 +9,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** * Product data needed for url rewrite generation locator class */ -class GetProductUrlRewriteDataByStore +class GetProductUrlRewriteDataByStore implements ResetAfterRequestInterface { /** * @var array @@ -51,8 +52,10 @@ public function execute(ProductInterface $product, int $storeId): array $storesData = $this->getUrlRewriteData->execute($product); foreach ($storesData as $storeData) { $this->urlRewriteData[$productId][$storeData['store_id']] = [ - 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), - 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], + 'visibility' => + (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), + 'url_key' => + $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], ]; } } @@ -73,4 +76,12 @@ public function clearProductUrlRewriteDataCache(ProductInterface $product) { unset($this->urlRewriteData[$product->getId()]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->urlRewriteData = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index e68b38b046afd..f82c8a99ac7f6 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -161,12 +161,18 @@ public function generateForGlobalScope($productCategories, Product $product, $ro Product::ENTITY )) { $mergeDataProvider->merge( - $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId) + $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId, true) ); } else { $scopedProduct = $this->productRepository->getById($productId, false, $id); $mergeDataProvider->merge( - $this->generateForSpecificStoreView($id, $productCategories, $scopedProduct, $rootCategoryId) + $this->generateForSpecificStoreView( + $id, + $productCategories, + $scopedProduct, + $rootCategoryId, + true + ) ); } } @@ -182,12 +188,20 @@ public function generateForGlobalScope($productCategories, Product $product, $ro * @param \Magento\Framework\Data\Collection|Category[] $productCategories * @param \Magento\Catalog\Model\Product $product * @param int|null $rootCategoryId + * @param bool $isGlobalScope * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + * @throws NoSuchEntityException */ - public function generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = null) - { + public function generateForSpecificStoreView( + $storeId, + $productCategories, + Product $product, + $rootCategoryId = null, + bool $isGlobalScope = false + ) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $categories = []; + foreach ($productCategories as $category) { if (!$this->isCategoryProperForGenerating($category, $storeId)) { continue; @@ -196,35 +210,29 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); } - $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); - $mergeDataProvider->merge( $this->canonicalUrlRewriteGenerator->generate($storeId, $product) ); - if ($this->isCategoryRewritesEnabled()) { - $mergeDataProvider->merge( - $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) - ); + $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); + + if ($isGlobalScope) { + $generatedUrls = $this->generateCategoryUrls((int) $storeId, $product, $productCategories); + } else { + $generatedUrls = $this->generateCategoryUrlsInStoreGroup((int) $storeId, $product, $productCategories); } + $mergeDataProvider->merge(array_merge(...$generatedUrls)); $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generate( + $this->currentUrlRewritesRegenerator->generateAnchor( $storeId, $product, $productCategories, $rootCategoryId ) ); - - if ($this->isCategoryRewritesEnabled()) { - $mergeDataProvider->merge( - $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) - ); - } - $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generateAnchor( + $this->currentUrlRewritesRegenerator->generate( $storeId, $product, $productCategories, @@ -252,6 +260,65 @@ public function isCategoryProperForGenerating(Category $category, $storeId) return false; } + /** + * Generate category URLs for the whole store group. + * + * @param int $storeId + * @param Product $product + * @param ObjectRegistry $productCategories + * + * @return array + * @throws NoSuchEntityException + */ + private function generateCategoryUrlsInStoreGroup( + int $storeId, + Product $product, + ObjectRegistry $productCategories + ): array { + $currentStore = $this->storeManager->getStore($storeId); + $currentGroupId = $currentStore->getStoreGroupId(); + $storeList = $this->storeManager->getStores(); + $generatedUrls = []; + + foreach ($storeList as $store) { + if ($store->getStoreGroupId() === $currentGroupId && $this->isCategoryRewritesEnabled()) { + $groupStoreId = (int) $store->getId(); + $generatedUrls[] = $this->generateCategoryUrls( + $groupStoreId, + $product, + $productCategories + ); + } + } + + return array_merge(...$generatedUrls); + } + + /** + * Generate category URLs. + * + * @param int $storeId + * @param Product $product + * @param ObjectRegistry $categories + * + * @return array + */ + private function generateCategoryUrls(int $storeId, Product $product, ObjectRegistry $categories): array + { + $generatedUrls[] = $this->categoriesUrlRewriteGenerator->generate( + $storeId, + $product, + $categories + ); + $generatedUrls[] = $this->anchorUrlRewriteGenerator->generate( + $storeId, + $product, + $categories + ); + + return $generatedUrls; + } + /** * Check if URL key has been changed * diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php index 244aaf4d5cdc9..7b49114f9609b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php @@ -103,13 +103,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) ScopeInterface::SCOPE_STORE, $category->getStoreId() ); + $catRewritesEnabled = $this->isCategoryRewritesEnabled(); + $category->setData('save_rewrites_history', $saveRewritesHistory); $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category, true); + + if ($catRewritesEnabled) { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + } + $this->urlRewriteHandler->deleteCategoryRewritesForChildren($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); - if ($this->isCategoryRewritesEnabled()) { - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + if ($catRewritesEnabled) { $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); } diff --git a/app/code/Magento/CatalogUrlRewrite/README.md b/app/code/Magento/CatalogUrlRewrite/README.md index a03229147129c..9d49b22319af1 100644 --- a/app/code/Magento/CatalogUrlRewrite/README.md +++ b/app/code/Magento/CatalogUrlRewrite/README.md @@ -1,6 +1,6 @@ # Magento_CatalogUrlRewrite module -This module generate url rewrite fields for catalog and product. +This module generate url rewrite fields for catalog and product. ## Extensibility @@ -8,4 +8,4 @@ Extension developers can interact with the Magento_CatalogUrlRewrite module. For [The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CatalogUrlRewrite module. -A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. \ No newline at end of file +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml index e1b59c07d187a..203e653cec882 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-69825"/> <group value="CatalogUrlRewrite"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml index d3471e0e4c0b0..4d0e2321b7952 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml @@ -13,6 +13,7 @@ <description value="Rewriting URL of product. Verify the full URL address"/> <severity value="MAJOR"/> <group value="CatalogUrlRewrite"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml index 26996223417b7..c05ef4c15e87f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml @@ -15,6 +15,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94934"/> <group value="CatalogUrlRewrite"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml index 749f713c1f34f..01cd451cf5e0a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-40780"/> <group value="catalog"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <before> <!-- Create categories --> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index e6a99bddcbc15..16cf430ac3307 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -166,6 +166,11 @@ public function testGenerationForGlobalScope() $product = $this->createMock(Product::class); $product->expects($this->any())->method('getStoreId')->willReturn(null); $product->expects($this->any())->method('getStoreIds')->willReturn([1]); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any())->method('getStoreGroupId')->willReturn(1); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') ->willReturn(true); $this->initObjectRegistryFactory([]); @@ -211,6 +216,11 @@ public function testGenerationForSpecificStore() $product = $this->createMock(Product::class); $product->expects($this->any())->method('getStoreId')->willReturn(1); $product->expects($this->never())->method('getStoreIds'); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any())->method('getStoreGroupId')->willReturn(1); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $this->categoryMock->expects($this->any())->method('getParentIds') ->willReturn(['root-id', $storeRootCategoryId]); $this->categoryMock->expects($this->any())->method('getId')->willReturn($category_id); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php index 843fb53914fee..7887995db956a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -89,14 +89,15 @@ protected function setUp(): void * Test category process rewrite url by changing the parent * * @return void + * @dataProvider getCategoryRewritesConfigProvider */ - public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() + public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId(bool $isCatRewritesEnabled) { /** @var Observer|MockObject $observerMock */ $observerMock = $this->createMock(Observer::class); $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() - ->setMethods(['getCategory']) + ->addMethods(['getCategory']) ->getMock(); $categoryMock = $this->createPartialMock( Category::class, @@ -108,17 +109,32 @@ public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() ] ); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') ->willReturn(true); - $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); - $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); $this->scopeConfigMock->expects($this->once())->method('isSetFlag') ->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true); - $this->scopeConfigMock->method('getValue')->willReturn(true); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('catalog/seo/generate_category_product_rewrites') + ->willReturn($isCatRewritesEnabled); + $this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate') ->with($categoryMock, true)->willReturn(['category-url-rewrite']); - $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites') - ->with($categoryMock)->willReturn(['product-url-rewrite']); + + if ($isCatRewritesEnabled) { + $this->urlRewriteHandlerMock->expects($this->once()) + ->id('generateProductUrlRewrites') + ->method('generateProductUrlRewrites') + ->with($categoryMock)->willReturn(['product-url-rewrite']); + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('deleteCategoryRewritesForChildren') + ->after('generateProductUrlRewrites'); + } else { + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('deleteCategoryRewritesForChildren'); + } $this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf(); $this->observer->execute($observerMock); @@ -135,7 +151,7 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() $observerMock = $this->createMock(Observer::class); $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() - ->setMethods(['getCategory']) + ->addMethods(['getCategory']) ->getMock(); $categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']); $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); @@ -145,4 +161,15 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() $this->observer->execute($observerMock); } + + /** + * @return array + */ + public function getCategoryRewritesConfigProvider(): array + { + return [ + [true], + [false] + ]; + } } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php index f1cec1c15d861..947237cbe084e 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; @@ -17,7 +18,7 @@ /** * Returns the url suffix for category */ -class CategoryUrlSuffix implements ResolverInterface +class CategoryUrlSuffix implements ResolverInterface, ResetAfterRequestInterface { /** * System setting for the url suffix for categories @@ -79,4 +80,12 @@ private function getCategoryUrlSuffix(int $storeId): ?string } return $this->categoryUrlSuffix[$storeId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->categoryUrlSuffix = []; + } } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php index db84784bab5b6..a91c7b4c966b7 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; @@ -17,7 +18,7 @@ /** * Returns the url suffix for product */ -class ProductUrlSuffix implements ResolverInterface +class ProductUrlSuffix implements ResolverInterface, ResetAfterRequestInterface { /** * System setting for the url suffix for products @@ -79,4 +80,12 @@ private function getProductUrlSuffix(int $storeId): ?string } return $this->productUrlSuffix[$storeId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productUrlSuffix = []; + } } diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index a8cafb034c0b0..d152b92a2d21c 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\ProductCategoryList; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\DB\Select; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -19,7 +20,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct +class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct implements ResetAfterRequestInterface { /** * @var string @@ -321,4 +322,12 @@ public function getBindArgumentValue() ) : $value; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->joinedAttributes = []; + } } diff --git a/app/code/Magento/CatalogWidget/README.md b/app/code/Magento/CatalogWidget/README.md index ea1951198c744..b80085640a2df 100644 --- a/app/code/Magento/CatalogWidget/README.md +++ b/app/code/Magento/CatalogWidget/README.md @@ -1,4 +1,5 @@ # CatalogWidget **CatalogWidget** contains various widgets that extend Catalog module functionality: + - Product List widget provides widget that contains product list created using rule based filter. diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 0addbf069cba3..3a2beb3b4371c 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -24,7 +24,7 @@ */ class Session extends \Magento\Framework\Session\SessionManager { - const CHECKOUT_STATE_BEGIN = 'begin'; + public const CHECKOUT_STATE_BEGIN = 'begin'; /** * Quote instance @@ -99,12 +99,12 @@ class Session extends \Magento\Framework\Session\SessionManager protected $customerRepository; /** - * @param QuoteIdMaskFactory + * @var QuoteIdMaskFactory */ protected $quoteIdMaskFactory; /** - * @param bool + * @var bool */ protected $isQuoteMasked; @@ -186,6 +186,19 @@ public function __construct( ->get(LoggerInterface::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_quote = null; + $this->_customer = null; + $this->_loadInactive = false; + $this->isLoading = false; + $this->_order = null; + } + /** * Set customer data. * diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index f397a8ddc9cf1..f08c48c55efa1 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Model; @@ -39,60 +40,62 @@ class ShippingInformationManagement implements ShippingInformationManagementInte /** * @var PaymentMethodManagementInterface */ - protected $paymentMethodManagement; + protected PaymentMethodManagementInterface $paymentMethodManagement; /** * @var PaymentDetailsFactory */ - protected $paymentDetailsFactory; + protected PaymentDetailsFactory $paymentDetailsFactory; /** * @var CartTotalRepositoryInterface */ - protected $cartTotalsRepository; + protected CartTotalRepositoryInterface $cartTotalsRepository; /** * @var CartRepositoryInterface */ - protected $quoteRepository; - + protected CartRepositoryInterface $quoteRepository; /** * @var Logger */ - protected $logger; + protected Logger $logger; /** * @var QuoteAddressValidator */ - protected $addressValidator; + protected QuoteAddressValidator $addressValidator; /** * @var AddressRepositoryInterface * @deprecated 100.2.0 + * @see AddressRepositoryInterface */ - protected $addressRepository; + protected AddressRepositoryInterface $addressRepository; /** * @var ScopeConfigInterface * @deprecated 100.2.0 + * @see ScopeConfigInterface */ - protected $scopeConfig; + protected ScopeConfigInterface $scopeConfig; /** * @var TotalsCollector * @deprecated 100.2.0 + * @see TotalsCollector */ - protected $totalsCollector; + protected TotalsCollector $totalsCollector; /** * @var CartExtensionFactory */ - private $cartExtensionFactory; + private CartExtensionFactory $cartExtensionFactory; /** * @var ShippingAssignmentFactory */ - protected $shippingAssignmentFactory; + protected ShippingAssignmentFactory $shippingAssignmentFactory; /** * @var ShippingFactory @@ -262,8 +265,11 @@ protected function validateQuote(Quote $quote): void * @param string $method * @return CartInterface */ - private function prepareShippingAssignment(CartInterface $quote, AddressInterface $address, $method): CartInterface - { + private function prepareShippingAssignment( + CartInterface $quote, + AddressInterface $address, + string $method + ): CartInterface { $cartExtension = $quote->getExtensionAttributes(); if ($cartExtension === null) { $cartExtension = $this->cartExtensionFactory->create(); diff --git a/app/code/Magento/Checkout/README.md b/app/code/Magento/Checkout/README.md index 942e35ec4d772..d4d45b9ea66fc 100644 --- a/app/code/Magento/Checkout/README.md +++ b/app/code/Magento/Checkout/README.md @@ -1,20 +1,23 @@ # Magento_Checkout module + Magento\Checkout module allows merchant to register sale transaction with the customer. Module implements consumer flow that includes such actions like adding products to cart, providing shipping and billing information and confirming the purchase. #### Observer + This module observes the following events `etc/events.xml` - `sales_quote_save_after` event in + `sales_quote_save_after` event in `Magento\Checkout\Observer\SalesQuoteSaveAfterObserver` file. `/etc/frontend/events.xml` `customer_login` event in `Magento\Checkout\Observer\LoadCustomerQuoteObserver` file. `customer_logout` event in `Magento\Checkout\Observer\UnsetAllObserver` - ### Layouts - The module interacts with the following layout handles in the +### Layouts + + The module interacts with the following layout handles in the `view/frontend/layout` `catalog_category_view` `catalog_product_view` @@ -30,4 +33,4 @@ the purchase. `checkout_onepage_failure` `checkout_onepage_review_item_renderers` `checkout_onepage_success` - `default` \ No newline at end of file + `default` diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml index 5cf9f009ba375..1f8a526a22d2d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml @@ -15,6 +15,6 @@ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> - <see userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> + <waitForText userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml index 1ec42033a782b..cf64a783644a4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml @@ -14,7 +14,7 @@ </annotations> <arguments> - <argument name="value" defaultValue="Not yet calculated" type="string"/> + <argument name="value" defaultValue="Selected shipping method is not available. Please select another shipping method for this order." type="string"/> </arguments> <waitForElementVisible selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" time="30" stepKey="waitForShippingTotalToBeVisible"/> <see selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" userInput="{{value}}" stepKey="assertShippingTotalIsNotYetCalculated"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..91b91e0e439b9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.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="CustomerLoggedInCheckoutFillNewBillingAddressActionGroup" extends="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <annotations> + <description>EXTENDS: LoggedInCheckoutFillNewBillingAddressActionGroup. Removes 'selectCountry' and 'selectState' to select state after country.</description> + </annotations> + + <remove keyForRemoval="selectCountry"/> + <remove keyForRemoval="selectState"/> + <selectOption stepKey="selectCountryOption" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{Address.country_id}}"/> + <selectOption stepKey="selectStateOption" selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="{{Address.state}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml index 527afdc26a5f4..be2c8989c67a9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml @@ -13,6 +13,7 @@ <argument name="customer" defaultValue="Simple_US_Customer" type="entity"/> <argument name="customerAddress" defaultValue="US_Address_TX" type="entity"/> </arguments> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForCustomerEmailField" /> <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customer.email}}" stepKey="setCustomerEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstname}}" stepKey="SetCustomerFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastname}}" stepKey="SetCustomerLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml index c9f315929dcfa..8132890d7937c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml @@ -20,7 +20,7 @@ <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> - <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street}}" stepKey="fillStreet"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street[0]}}" stepKey="fillStreet"/> <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{address.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}" stepKey="fillZipCode"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml index 0c97a81b1c0d7..ee06ec3ef1b4d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml @@ -36,6 +36,6 @@ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForPaymentLoading"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml index 9d7c72522d003..d6c2e90df9a9e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml @@ -29,7 +29,7 @@ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.noQuotes}}" stepKey="waitMessage"/> <see userInput="No Payment method available." stepKey="checkMessage"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml index ada3674a07b8d..b39816588e904 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml @@ -30,6 +30,6 @@ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml index 441e3571d0f55..51214a2a7c6b0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml @@ -37,6 +37,6 @@ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForPaymentLoading"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml index 0c4cea142b4e6..4b16c0db2e09a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml @@ -30,6 +30,6 @@ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index 4b6680442a470..5a6aae21719f9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -30,6 +30,6 @@ <waitForPageLoad stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml index 95d78777ed922..aff2bef433939 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml @@ -25,7 +25,7 @@ <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml index f13850357b182..077e4eb960294 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml @@ -8,11 +8,14 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCheckoutClickNextOnShippingStepActionGroup" extends="StorefrontCheckoutForwardFromShippingStepActionGroup"> + <actionGroup name="StorefrontCheckoutClickNextOnShippingStepActionGroup"> <annotations> <description>Scrolls and clicks next on Checkout Shipping step</description> </annotations> - <scrollTo selector="{{CheckoutShippingSection.next}}" before="clickNext" stepKey="scrollToNextButton"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButtonElement"/> + <scrollTo selector="{{CheckoutShippingSection.next}}" stepKey="scrollToNextButton"/> + <waitForElementClickable selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml index 524e3f784ed3f..9b2101e2fd8f9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml @@ -8,11 +8,12 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCheckoutForwardFromShippingStepActionGroup"> + <actionGroup name="StorefrontCheckoutForwardFromShippingStepActionGroup" deprecated="[DEPRECATED] Please use StorefrontCheckoutClickNextOnShippingStepActionGroup"> <annotations> <description>Clicks next on Checkout Shipping step</description> </annotations> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForElementClickable selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml index a55db2b92e9c3..2d8be3ec50d69 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml @@ -15,6 +15,6 @@ <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded" after="clickNext"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml index bf82d4cf20b1b..ee91191a63352 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml @@ -9,11 +9,14 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Fill shipment form for free shipping--> - <actionGroup name="ShipmentFormFreeShippingActionGroup"> + <actionGroup name="ShipmentFormFreeShippingActionGroup" deprecated="This action group must not be used because it violated Technical guidelines on how to write tests."> <annotations> <description>Fills in the Customer details for the 'Shipping Address' section of the Storefront Checkout page. Selects 'Free Shipping'. Clicks on Next. Validates that the URL is present and correct.</description> </annotations> + <!-- [DO NOT USE!] This action group must not be used because it violated Technical guidelines on how to write tests. --> + <!-- Instead use combination of FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup, StorefrontSetShippingMethodActionGroup, StorefrontCheckoutClickNextOnShippingStepActionGroup --> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="SetCustomerFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="SetCustomerLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 84f9a7930d40b..c3c3a5f855f4c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -54,5 +54,6 @@ <!-- Required attention section --> <element name="removeProductBySku" type="button" selector="//div[contains(., '{{sku}}')]/ancestor::tbody//button" parameterized="true" timeout="30"/> <element name="failedItemBySku" type="block" selector="//div[contains(.,'{{sku}}')]/ancestor::tbody" parameterized="true" timeout="30"/> + <element name="attributeText" selector="//tbody[@class='cart item']//a[text()='{{product_name}}']/../..//dl//dt[text()='{{attribute_name}}']/..//dd[contains(text(),'{{attribute_option}}')]" type="text" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml index 13db791d3f474..95aad2a9ddf92 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.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="CheckoutShippingMethodsSection"> + <element name="shippingMethodSelectorNextButton" selector="#checkout-step-shipping_method button.button.action.continue.primary" type="button" timeout="30" /> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> <element name="shippingMethodRow" type="text" selector=".form.methods-shipping table tbody tr"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 581c0976e6d71..edc9e029264db 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -28,6 +28,7 @@ <element name="city" type="input" selector="input[name=city]"/> <element name="region" type="select" selector="select[name=region_id]"/> <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="invalidPostcodeJSError" type="text" selector="//span[@data-bind='text: element.warn']"/> <element name="country" type="select" selector="select[name=country_id]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="saveAddress" type="button" selector=".action-save-address"/> @@ -51,5 +52,6 @@ <element name="stateProvince" type="text" selector="//div[@name='shippingAddress.region_id']//span[contains(text(),'State/Province')]" timeout="30"/> <element name="stateProvinceWithoutAsterisk" type="text" selector="//div[@class='field' and @name='shippingAddress.region_id']" timeout="30"/> <element name="stateProvinceWithAsterisk" type="text" selector="//div[@class='field _required' and @name='shippingAddress.region_id']" timeout="30"/> + <element name="selectCountry" type="select" selector="//div[@class='billing-address-form']//select[@name='country_id']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml index abdb4ddac7343..e5e912af73343 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -21,5 +21,6 @@ <element name="printLink" type="button" selector=".print" timeout="30"/> <element name="orderNumberWithoutLink" type="text" selector="//div[contains(@class, 'checkout-success')]//p/span"/> <element name="orderLinkByOrderNumber" type="text" selector="//div[contains(@class,'success')]//a[contains(.,'{{orderNumber}}')]" parameterized="true" timeout="30"/> + <element name="purchaseOrderNumber" type="text" selector="div.checkout-success > p:nth-child(1) > a span"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index 9bb999d643add..2e587e3f7962b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -48,6 +48,8 @@ <element name="productCountLabel" type="text" selector="//*[@id='minicart-content-wrapper']/div[2]/div[1]/span[2]"/> <element name="productCartName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']//a[contains(text(),'{{var}}')]" parameterized="true"/> <element name="minicartclose" type="button" selector="//button[@id='btn-minicart-close']"/> + <element name="productCountNew" type="text" selector=".minicart-wrapper .action.showcart .counter-number"/> <element name="image" type="text" selector="//*[@class='product-image-container']//img[contains(@src, '{{var1}}')]" parameterized="true"/> + <element name="proceedToCheckout" type="button" selector="//button[@data-role='proceed-to-checkout']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 5a065e5dead9c..55534aff2aefe 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -58,7 +58,7 @@ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml index eddb7d430387c..278b1b3f36965 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-6223"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml index 12a524d2d6ad8..1f97e2230ad0f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-94178"/> <useCaseId value="MAGETWO-71375"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -62,7 +63,17 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml index f08640d895b6b..b19c34cafd30e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml @@ -24,12 +24,6 @@ <createData entity="SimpleProduct" stepKey="simpleproduct"> <requiredEntity createDataKey="simplecategory"/> </createData> - <createData entity="PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> - <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> - <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> - <argument name="tags" value="config full_page"/> - </actionGroup> - <createData entity="ApiSalesRule" stepKey="createCartPriceRule"> <field key="discount_amount">100</field> </createData> @@ -37,15 +31,20 @@ <requiredEntity createDataKey="createCartPriceRule"/> </createData> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="freeShippingMethodsSettingConfig"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- To be removed when BI Changes are allowed --> + <comment userInput="Preserve BIC. PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> + <comment userInput="Preserve BIC. CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches" /> </before> <after> + <magentoCLI command="config:set {{EnableFreeOrderStatusPending.path}} {{EnableFreeOrderStatusPending.value}}" stepKey="disablePaymentMethodsSettingConfig"/> + <magentoCLI command="config:set {{EnableFreeOrderPaymentAutomaticInvoiceAction.path}} {{EnableFreeOrderPaymentAutomaticInvoiceAction.value}}" stepKey="enableFreeOrderPaymentAutomaticInvoiceAction"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> <deleteData createDataKey="simpleproduct" stepKey="deleteProduct"/> - <createData entity="DisablePaymentMethodsSettingConfig" stepKey="disablePaymentMethodsSettingConfig"/> - <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> - <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteSalesRule"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> @@ -54,6 +53,8 @@ <argument name="tags" value="config full_page"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <!-- To be removed when BI Changes are allowed --> + <comment userInput="Preserving BIC. DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> </after> <!-- Go to new order status page --> @@ -62,29 +63,31 @@ <!-- Fill the form and validate message --> <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> - <argument name="status" value="{{EnableFreeOrderStatusCustom.value}}"/> - <argument name="label" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> </actionGroup> <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> <!-- Verify the order status grid page shows the order status we just created --> <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> - <argument name="status" value="{{EnableFreeOrderStatusCustom.value}}"/> - <argument name="label" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> </actionGroup> <!-- Assign status to state --> <click selector="{{AdminOrderStatusGridSection.assignStatusToStateBtn}}" stepKey="clickAssignStatusBtn"/> - <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderStatus}}" userInput="{{EnableFreeOrderStatusCustom.value}}" stepKey="selectOrderStatus"/> + <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderStatus}}" userInput="{{defaultOrderStatus.label}}" stepKey="selectOrderStatus"/> <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderState}}" userInput="{{OrderState.new}}" stepKey="selectOrderState"/> <checkOption selector="{{AdminAssignOrderStatusToStateSection.orderStatusAsDefault}}" stepKey="orderStatusAsDefault"/> <uncheckOption selector="{{AdminAssignOrderStatusToStateSection.visibleOnStorefront}}" stepKey="visibleOnStorefront"/> <click selector="{{AdminAssignOrderStatusToStateSection.saveStatusAssignment}}" stepKey="clickSaveStatus"/> - <see selector="{{AdminMessagesSection.success}}" userInput="You assigned the order status." stepKey="seeSuccess"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccess"> + <argument name="message" value="You assigned the order status." /> + </actionGroup> <!-- Prepare data for constraints --> - <magentoCLI command="config:set {{EnableFreeOrderStatusCustom.path}} {{EnableFreeOrderStatusCustom.value}}" stepKey="enableNewOrderStatus"/> - <magentoCLI command="config:set {{EnableFreeOrderPaymentAction.path}} {{EnableFreeOrderPaymentAction.value}}" stepKey="enableNewOrderPaymentAction"/> + <magentoCLI command="config:set {{EnableFreeOrderStatusCustom.path}} {{defaultOrderStatus.status}}" stepKey="enableNewOrderStatus"/> + <magentoCLI command="config:set {{DisableFreeOrderPaymentAutomaticInvoiceAction.path}} {{DisableFreeOrderPaymentAutomaticInvoiceAction.value}}" stepKey="enableNewOrderPaymentAction"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> @@ -94,9 +97,20 @@ <argument name="product" value="$$simpleproduct$$"/> </actionGroup> - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="clickToProceedToCheckout"/> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.shippingMethodSelectorNextButton}}" stepKey="waitForNextButtonVisible" /> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl" /> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> @@ -113,7 +127,7 @@ <actionGroup ref="AdminCheckOrderStatusInGridActionGroup" stepKey="seeOrderStatusInGrid"> <argument name="orderId" value="$grabOrderNumber"/> - <argument name="status" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.label}}"/> </actionGroup> <!-- Open order --> @@ -121,8 +135,10 @@ <argument name="orderId" value="{$grabOrderNumber}"/> </actionGroup> - <!-- Assert invoice button --> - <seeElement selector="{{AdminOrderDetailsMainActionsSection.invoiceBtn}}" stepKey="seeInvoiceBtn"/> - + <!-- Assert Order Status on Order view page --> + <waitForElementVisible selector="{{AdminOrderDetailsMainActionsSection.invoiceBtn}}" stepKey="seeInvoiceBtn"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="verifyOrderStatusOnOrderViewPage"> + <argument name="status" value="{{defaultOrderStatus.label}}" /> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml index 81de8664f98e2..1a32120bad3bb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml @@ -63,7 +63,17 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml index 979976caf78ae..e3cbad4ad2797 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml @@ -15,6 +15,7 @@ <description value="Assert success message appears after adding product to cart that contains out of stock product"/> <severity value="MINOR"/> <testCaseId value="AC-5613"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml index 9a3a590952467..e336a65e61c6a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16490"/> <group value="checkout"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml index 54ac1143b3573..9adbf4dd2e36e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml @@ -16,6 +16,7 @@ <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> <severity value="MAJOR"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <!--Create simple product1 and simple product2--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml index 92a4b9563ab3d..39bd49b196302 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml @@ -15,6 +15,7 @@ <description value="Verify that disabling the clear shopping cart store configuration will remove the clear shopping cart configuration button from the storefront's shopping cart page. Verify that enabling the configuration will add the button to the page and that the button functions as expected"/> <group value="shoppingCart"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create simple products and category --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml index 824fb9e063038..cc89dbacc66ea 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -34,7 +34,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Logout from customer account--> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml index a30f118bd6207..efc29bb6a4e30 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14689"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and simple product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml index 9958b12ceaf25..467ce4c963d13 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14690"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create simple product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml index b82df28ebb95f..1a63544c44bc8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14694"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create grouped product with three simple products --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml index 39b4e66ef9f07..f37f6cba72194 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14691"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create virtual product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index d45fb92744544..694c3c70b7fff 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14680"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml index 64f392d39edcb..f9ddee8284d8b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml @@ -18,13 +18,14 @@ <testCaseId value="MC-14741"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">560</field> </createData> - <!-- Create customer --> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> </before> @@ -40,6 +41,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Add Simple Product to cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml index 138fbe5055d61..5f5b90d11d132 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14740"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml index f6db22cbccaa8..8ccb7af5a334a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml @@ -18,8 +18,10 @@ <testCaseId value="MC-14739"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">560</field> @@ -40,6 +42,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Add Simple Product to cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml index ecd1e91a62a3a..69f7dab981e50 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-28548"/> <useCaseId value="MAGETWO-96431"/> <group value="Checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml index e8bb89e0f112d..b3b0c993bca63 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14738"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml index 2e1c8d5a27886..ab003f05bb392 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42729"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml index 68dcf6600f49a..6ef158f8f371b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14725"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml index c29e19275f759..11b78553d3f90 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-28548"/> <useCaseId value="MAGETWO-96431"/> <group value="Checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml index a9d34db16b506..88eb7c2e417a9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml @@ -16,6 +16,7 @@ <stories value="Guest Checkout"/> <testCaseId value="AC-4604"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!--PRECONDITIONS--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index 678929ff228c2..e1e0a307f9cd8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14715"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml index a403f928229bb..a25040497c2c7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14726"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml index feab5625c115d..4778a4024e4d1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml @@ -56,6 +56,8 @@ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </after> <!--Open Product page in StoreFront --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml index 7e142597e47b0..427cf230da742 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-43255"/> <group value="checkout"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 6b9c1f8f9b009..2e06d1533e65b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25694"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml index 024e1221d95e6..aa5a3511e00e0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml @@ -37,7 +37,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Disable Cash On Delivery method--> <actionGroup ref="CashOnDeliverySpecificCountryActionGroup" stepKey="disableCashOnDelivery"/> <!--Customer log out--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml index 736e045f588aa..8b4720ad7c26c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="checkout"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml new file mode 100644 index 0000000000000..7aa0259d43621 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest"> + <annotations> + <stories value="Validate mini cart decimal quantities items in cart"/> + <title value="Checking by adding decimal quantities in mini cart"/> + <description value="Checking by adding decimal quantities in mini cart"/> + <testCaseId value="AC-7554"/> + <severity value="AVERAGE"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Set Display Cart Summary to display items quantities--> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="setDisplayCartSummary"/> + <!--Create simple product--> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="resetDisplayCartSummary"/> + </after> + <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin" /> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createPreReqSimpleProduct.name$$')}}" stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="0.5" stepKey="fillMinAllowedQty"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> + <!-- Add simpleProduct to cart --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createPreReqSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addProduct2ToCart"> + <argument name="productName" value="$$createPreReqSimpleProduct.name$$"/> + <argument name="productQty" value="0.5"/> + </actionGroup> + <!-- Open Mini Cart --> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> + <!-- Assert Products Count in Mini Cart --> + <see selector="{{StorefrontMinicartSection.productCountNew}}" userInput="0.5" stepKey="seeProductCountInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml index 83ed32803654e..a64e667acb702 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-18281"/> <severity value="CRITICAL"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index e31db8ee28c7f..13c65c7242f96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14720"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml index c68961c3e8c2b..a6368d71c28b9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14721"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml index e769d9d37286d..53b1a0938e355 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-28550"/> <useCaseId value="MAGETWO-95820"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml index 93d1c4092c05e..386360e2cdbcb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14700"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set the default number of items on cart which is 20--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml index ee32ce6d928a1..dd8675e4e8533 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14723"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml index 743f4e0165159..f0650fb187d1c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-29105"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml index acb274886a6c8..da73a2f62b96d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml @@ -67,7 +67,7 @@ <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="goToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml index 1a85bb0bee1ee..a176a7ccd27f3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml @@ -18,6 +18,7 @@ </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> @@ -101,10 +102,14 @@ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Remove Filter--> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!--Open Product page in StoreFront and assert product and price range --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml index 68842ee09a855..1926637257213 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-21996"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index a8c694f4a8436..6909ff4a9fa2a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-30274"/> <group value="checkout"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -53,7 +54,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml index f12dd6fb34827..c2645f3e4d41a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -60,7 +60,7 @@ <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectAddress"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextButton"/> <waitForPageLoad stepKey="waitBillingForm"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> <!-- Fill UK Address and verify that payment available and checkout successful --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml index 28e779f802cde..266c8210c273c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml @@ -65,7 +65,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml index eb76748a81c97..d59af6328f736 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml @@ -17,8 +17,10 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13097"/> <group value="OnePageCheckout"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create simple product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> @@ -39,9 +41,9 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> - <!-- Logout admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Go to Storefront as Guest and create new account --> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml index d289bdc0dc8d1..97aa966aa4ad5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Shopping Cart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml index 2d6f36c78edf6..ddf65a0922bbf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Shopping Cart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml index 45eb3443e4205..2a51c69d4a691 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-96979"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml index da4a1b93691b6..5879131ff91a7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-12825"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml index d6b27b73601e8..261f03308d818 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-27419"/> <group value="module-checkout"/> + <group value="cloud"/> </annotations> <before> <!-- create category and simple product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml index a5a3675ea0a0b..287c737e24e67 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14698"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set the default number of items on cart which is 20--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml index 2a81ff2b64186..7ded05eda92f0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-96960"/> <useCaseId value="MAGETWO-96850"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <!--Create a product--> @@ -36,6 +37,7 @@ <argument name="productName" value="$createProduct.name$$"/> </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible" /> <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> @@ -60,7 +62,7 @@ <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <!--Go to cart page, update qty and proceed to checkout--> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml index 66a4f417aed9d..98dfc2b4f26a0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-18312" /> <group value="shoppingCart" /> <group value="mtf_migrated" /> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 3ab3a0b4ad3f7..5cb5b375a3967 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -22,13 +22,13 @@ <createData entity="SimpleProduct2" stepKey="createProduct"> <field key="price">10</field> </createData> - <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShippingMethod"/> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </before> <after> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShippingMethod"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> </after> <!-- 1. Add simple product to cart and go to checkout--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml new file mode 100644 index 0000000000000..d4163b1dae357 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via register customer"/> + <title value="Persistent Data for register Customer with virtual quote"/> + <description value="One can use Persistent Data for register Customer with virtual quote"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4166"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnablePaymentCheckMOConfigData.path}} {{EnablePaymentCheckMOConfigData.value}}" stepKey="enableCheckMoneyOrderPayment"/> + <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> + </before> + <after> + <!-- delete created data --> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableCheckMoneyOrderPaymentMethod.path DisableCheckMoneyOrderPaymentMethod.value" stepKey="disableCheckMoneyOrderPaymentMethod"/> + </after> + <!-- Login as Customer Login from Customer page --> + <!--Login to Frontend--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Add default address --> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="addNewDefaultAddress"> + <argument name="Address" value="US_Address_California"/> + </actionGroup> + <!--Add product to cart.--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$createVirtualProduct$"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCart"/> + <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="California" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="90230" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United Kingdom" stepKey="selectCountry"/> + <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="KW1 7NQ" stepKey="fillZipPostalCodeField"/> + <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> + <dontSeeJsError stepKey="verifyThatThereIsNoJSErrors"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForpageload"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="United Kingdom" stepKey="assertCountryFieldInCartEstimateShippingSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="" stepKey="assertStateProvinceInCartEstimateShippingSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="KW1 7NQ" stepKey="assertZipPostalCodeInCartEstimateShippingSection"/> + <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="goToCheckout"/> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPage"> + <argument name="customerVar" value="$$createCustomer$$" /> + <argument name="customerAddressVar" value="US_Address_California" /> + </actionGroup> + <conditionalClick selector="{{CheckoutShippingSection.editActiveAddressButton}}" dependentSelector="{{CheckoutShippingSection.editActiveAddressButton}}" visible="true" stepKey="clickEditButton"/> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="editAddress"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="waitForDropDownToBeVisible"/> + <selectOption selector="{{CheckoutShippingSection.addressDropdown}}" userInput="New Address" stepKey="addAddress"/> + <waitForPageLoad stepKey="waitForMaskLoading"/> + <seeInField stepKey="fillFirstName" selector="{{CheckoutShippingSection.firstName}}" userInput="John"/> + <seeInField stepKey="fillLastName" selector="{{CheckoutShippingSection.lastName}}" userInput="Doe"/> + <wait time="10" stepKey="waitForSelectCountry"/> + <seeOptionIsSelected selector="{{CheckoutShippingSection.selectCountry}}" userInput="{{UK_Address.country}}" stepKey="seeCountryIsUnitedKingdom"/> + <seeInField stepKey="fillZip" selector="{{CheckoutShippingSection.postcode}}" userInput="KW1 7NQ"/> + <actionGroup ref="CustomerLoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeBillingAddress"> + <argument name="Address" value="Switzerland_Address"/> + <argument name="classPrefix" value="[aria-hidden=false]"/> + </actionGroup> + <!-- Check order summary in checkout --> + <actionGroup ref="StorefrontClickUpdateAddressInCheckoutActionGroup" stepKey="clickToUpdate"/> + <comment userInput="BIC workaround" stepKey="waitForPageLoading"/> + <reloadPage stepKey="againRefreshPage1"/> + <wait time="10" stepKey="waitForPageLoad"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="AgainGoToShoppingCart"/> + <dontSeeJsError stepKey="againVerifyThatThereIsNoJSErrors"/> + <conditionalClick selector="{{CheckoutShippingSection.editActiveAddressButton}}" dependentSelector="{{CheckoutShippingSection.editActiveAddressButton}}" visible="true" stepKey="againClickEditButton"/> + <waitForPageLoad stepKey="againWaitForLoadingMask"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="{{Switzerland_Address.country}}" stepKey="againAssertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="{{Switzerland_Address.state}}" stepKey="againAssertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{Switzerland_Address.postcode}}" stepKey="againAssertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml index 87eba009f5e56..83f410c7dc369 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-12084"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml index 44bfe81b40dc0..7dddc2d238122 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="https://github.com/magento/magento2/issues/23460"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml index f0c3a23a8d39c..b6d193dd4a6a3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml @@ -18,6 +18,7 @@ <severity value="BLOCKER"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml index afb4ff03a4fc9..291c22408ba09 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml @@ -18,6 +18,7 @@ <severity value="BLOCKER"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml index 65f5dd365b215..4a90b01a700b1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml @@ -20,6 +20,7 @@ <group value="checkout"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml index e7dd7a0db223f..e1a2a6d97b7ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -18,6 +18,7 @@ <group value="mtf_migrated"/> <group value="checkout"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml index 8fc37bdaafdee..918c737f67de5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="shoppingCart"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <!-- Enable MAP functionality in Magento Instance --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml new file mode 100644 index 0000000000000..3317a72e9f610 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyZipCodeWorkingAsPerCountryTest"> + <annotations> + <features value="Checkout"/> + <stories value="Guest checkout"/> + <title value="Storefront Verify ZipCode Working As Per Country"/> + <description value="Storefront Verify ZipCode Working As Per Country"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4016"/> + </annotations> + <before> + <!-- create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- create simple product --> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Step 1: Go to Storefront as Guest --> + <!-- Step 2: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($createProduct.custom_attributes[url_key]$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <!-- Proceed to Checkout --> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickToOpenCard"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickToProceedToCheckout"/> + <waitForPageLoad stepKey="waitForTheFormIsOpened"/> + <!-- verify shipping screen is opened --> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened"/> + <!-- Enter invalid zip code as "1" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="1" stepKey="SetCustomerZipCode"/> + <!-- wait for JS error message to appear --> + <waitForElementVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForElementVisible"/> + <see selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" userInput="Provided Zip/Postal Code seems to be invalid. Example: 12345-6789; 12345. If you believe it is the right one you can ignore this notice." stepKey="seeErrorMessage"/> + + <!-- Enter valid zip code as "12345-6789" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="12345-6789" stepKey="SetCustomerZipCode123456789"/> + <!-- wait for JS error message to disappear --> + <waitForElementNotVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForElementNotVisible"/> + <!-- Enter invalid zip code as "abc" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="abc" stepKey="SetCustomerZipCodeabc"/> + + <!-- wait for JS error message to appear --> + <waitForElementVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForJSElementMessageVisible"/> + <see selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" userInput="Provided Zip/Postal Code seems to be invalid. Example: 12345-6789; 12345. If you believe it is the right one you can ignore this notice." stepKey="seeJSErrorMessage"/> + <!-- change country as United Kingdom" --> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{updateCustomerUKAddress.country_id}}" stepKey="selectCountry"/> + <!-- Enter valid zip code as "A12 3BC" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="A12 3BC" stepKey="SetCustomerZipCodeA123BC"/> + <!-- wait for JS error message to disappear --> + <waitForElementNotVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForJSErrorElementNotVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml index 41b5f734d0096..af275e148102f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml @@ -16,6 +16,7 @@ <description value="Guest should not be able to see password field if entered unregistered email"/> <severity value="MINOR"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml index 6e484c30fa81e..4ee8a0b1c209e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-15068"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml index 97906cade542c..bfa8557698a8e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-42907"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml index 6016893ed3e28..95fe1db043e1e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml @@ -63,7 +63,7 @@ <!--Do the payment and place the order--> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <seeElement selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="seeOrderNumber"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml new file mode 100644 index 0000000000000..3167bf1df03a2 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest"> + <annotations> + <features value="Checkout"/> + <stories value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <title value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <description value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4588"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <actionGroup ref="AdminAllowToChooseStateActionGroup" stepKey="disableAllowState"> + <argument name="fieldValue" value="0"/> + </actionGroup> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <actionGroup ref="AdminAllowToChooseStateActionGroup" stepKey="enableAllowState"> + <argument name="fieldValue" value="1"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$" /> + <argument name="productCount" value="1" /> + </actionGroup> + <!-- go to shopping cart and asser states --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart" /> + <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <seeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="assertUSStateProvince"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="Tajikistan" stepKey="selectTJKCountry"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="dontSeeTJKStateProvince"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="France" stepKey="selectFRCountry"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="dontSeeFRStateProvince"/> + <!-- go to shipping page and assert states --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="Tajikistan" stepKey="selectTJCountry"/> + <dontSeeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="dontSeeTJStateProvince"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="France" stepKey="selectFranceCountry"/> + <dontSeeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="dontSeeFranceStateProvince"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United States" stepKey="selectUStatesCountry"/> + <seeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="seeUSStateProvince"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillGuestShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFirstShippingMethod"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml index 66f8e327b9d22..71bb7d1e65954 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -19,7 +19,7 @@ <group value="checkout"/> <skip> <issueId value="DEPRECATED">Use AdminCheckZeroSubtotalOrderIsInProcessingStatusTest instead</issueId> - </skip> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -74,7 +74,17 @@ <waitForPageLoad stepKey="waitForTheFormIsOpened"/> <!--Fill shipping form--> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToAddDiscount"/> <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{_defaultCoupon.code}}" stepKey="TypeDiscountCode"/> diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index b56566a043c3e..5bb0f37f3bc25 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -13,6 +13,11 @@ <resource>Magento_Checkout::checkout</resource> <group id="options" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Checkout Options</label> + <field id="enable_guest_checkout_login" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enable Guest Checkout Login</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Enabling this setting will allow unauthenticated users to query if an e-mail address is already associated with a customer account. This can be used to enhance the checkout workflow for guests that do not realize they already have an account but comes at the cost of exposing information to unauthenticated users.</comment> + </field> <field id="onepage_checkout_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enable Onepage Checkout</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -23,7 +28,7 @@ </field> <field id="display_billing_address_on" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Display Billing Address On</label> - <source_model>\Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions</source_model> + <source_model>Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions</source_model> </field> <field id="max_items_display_count" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Maximum Number of Items to Display in Order Summary</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index eac0bd849da35..c85d68b35f714 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -9,6 +9,7 @@ <default> <checkout> <options> + <enable_guest_checkout_login>0</enable_guest_checkout_login> <onepage_checkout_enabled>1</onepage_checkout_enabled> <guest_checkout>1</guest_checkout> <display_billing_address_on>0</display_billing_address_on> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index aa3cf0748cb0c..7e87fe3f7e009 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -176,6 +176,7 @@ Summary,Summary "We'll send your order confirmation here.","We'll send your order confirmation here." Payment,Payment "Not yet calculated","Not yet calculated" +"Selected shipping method is not available. Please select another shipping method for this order.","Selected shipping method is not available. Please select another shipping method for this order." "The order was not successful!","The order was not successful!" "Thank you for your purchase!","Thank you for your purchase!" "Password", "Password" diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index b20b4d02706f3..411726607c669 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -373,7 +373,7 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/shipping</item> <item name="config" xsi:type="array"> <item name="title" xsi:type="string" translate="true">Shipping</item> - <item name="notCalculatedMessage" xsi:type="string" translate="true">Not yet calculated</item> + <item name="notCalculatedMessage" xsi:type="string" translate="true">Selected shipping method is not available. Please select another shipping method for this order.</item> </item> </item> <item name="grand-total" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index d23e220aa9942..ab2e495730a11 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -38,7 +38,7 @@ <!-- ko if: (getCartParam('summary_count') > 1) --> <span translate="'Items in Cart'"></span> <!--/ko--> - <!-- ko if: (getCartParam('summary_count') === 1) --> + <!-- ko if: (getCartParam('summary_count') <= 1) --> <span translate="'Item in Cart'"></span> <!--/ko--> </div> diff --git a/app/code/Magento/CheckoutAgreements/README.md b/app/code/Magento/CheckoutAgreements/README.md index 3d31bffd1b542..628bfa165013a 100644 --- a/app/code/Magento/CheckoutAgreements/README.md +++ b/app/code/Magento/CheckoutAgreements/README.md @@ -1,3 +1,3 @@ Magento\CheckoutAgreements module provides the ability add web store agreement that customers must accept before purchasing products from store. The customer will need to accept the terms and conditions in the Order Review section of the -checkout process to be able to place an order if Terms and Conditions functionality is enabled. \ No newline at end of file +checkout process to be able to place an order if Terms and Conditions functionality is enabled. diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml new file mode 100644 index 0000000000000..fec0a686d839e --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.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="AdminDeleteAllTermConditionsActionGroup"> + <annotations> + <description>Deletes all rows one by one on the 'Terms and Conditions' page.</description> + </annotations> + <waitForElementVisible selector="{{AdminLegacyDataGridFilterSection.clear}}" stepKey="waitForResetFilter"/> + <click selector="{{AdminLegacyDataGridFilterSection.clear}}" stepKey="clickResetFilter"/> + <waitForPageLoad stepKey="waitForGridReset"/> + <helper class="Magento\CheckoutAgreements\Test\Mftf\Helper\CheckoutAgreementsHelpers" method="deleteAllTermConditionRows" stepKey="deleteAllTermConditionRows"> + <argument name="rowsToDelete">{{AdminTermGridSection.allTermRows}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="successMessage">You deleted the condition.</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + </helper> + <waitForPageLoad stepKey="waitForGridLoad"/> + <waitForText userInput="We couldn't find any records." selector="{{AdminTermGridSection.emptyGrid}}" stepKey="waitForEmptyGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml new file mode 100644 index 0000000000000..3cddd2ebb5389 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.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="AdminOpenEditPageTermsConditionsByNameActionGroup"> + <annotations> + <description>Opens Edit Page of Terms and Conditions By Provided Name</description> + </annotations> + <arguments> + <argument name="termName" type="string"/> + </arguments> + + <fillField selector="{{AdminTermGridSection.filterByTermName}}" userInput="{{termName}}" stepKey="fillTermNameFilter"/> + <click selector="{{AdminTermGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <grabAttributeFrom selector="{{AdminTermGridSection.firstRow}}" userInput="title" stepKey="termsEditUrl" /> + <amOnUrl url="{$termsEditUrl}" stepKey="openTermsEditPage" /> + <waitForPageLoad stepKey="waitForEditTermPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml index fe40a92948cc5..8f2e65415ac22 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml @@ -13,7 +13,7 @@ <description>Filters Terms and Conditions grid and opens the first result Edit page</description> </annotations> - <doubleClick selector="{{AdminTermGridSection.firstRowConditionId}}" stepKey="clickFirstRow"/> + <click selector="{{AdminTermGridSection.firstRowConditionId}}" stepKey="clickFirstRow"/> <waitForPageLoad stepKey="waitForEditTermPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml index bc0c48142e223..0f25fcf84e4b6 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml @@ -30,6 +30,6 @@ <waitForElementNotVisible selector=".loading-mask" time="300" stepKey="waitForProcessShippingMethod"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml index 0172ffc771384..5fd439c0ce244 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml @@ -44,4 +44,13 @@ <data key="checkboxText" unique="suffix">test_checkbox</data> <data key="content"><html></data> </entity> + <entity name="newHtmlTerm" type="term"> + <data key="name" unique="suffix">Test name</data> + <data key="isActive">Enabled</data> + <data key="isHtml">Text</data> + <data key="mode">Manually</data> + <data key="storeView">All Store Views</data> + <data key="checkboxText" unique="suffix">test_checkbox</data> + <data key="content" unique="suffix">TestMessage</data> + </entity> </entities> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php new file mode 100644 index 0000000000000..7f0150b274f47 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CheckoutAgreements\Test\Mftf\Helper; + +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Exception; + +/** + * Class for MFTF helpers for CheckoutAgreements module. + */ +class CheckoutAgreementsHelpers extends Helper +{ + /** + * Delete all term conditions one by one from the Terms & Conditions grid page. + * + * @param string $rowsToDelete + * @param string $deleteButton + * @param string $modalAcceptButton + * @param string $successMessage + * @param string $successMessageContainer + * + * @return void + */ + public function deleteAllTermConditionRows( + string $rowsToDelete, + string $deleteButton, + string $modalAcceptButton, + string $successMessage, + string $successMessageContainer + ): void { + try { + /** @var MagentoWebDriver $magentoWebDriver */ + $magentoWebDriver = $this->getModule("\\" . MagentoWebDriver::class); + $webDriver = $magentoWebDriver->webDriver; + + $magentoWebDriver->waitForPageLoad(30); + $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($deleteButton, 10); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForText($successMessage, 10, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete)); + } + } catch (Exception $exception) { + $this->fail($exception->getMessage()); + } + } +} diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml index 326f9dcce4320..b80a4b83c502c 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml @@ -11,8 +11,11 @@ <element name="searchButton" type="button" selector="//div[contains(@class,'admin__data-grid-header')]//div[contains(@class,'admin__filter-actions')]/button[1]"/> <element name="resetButton" type="button" selector="//div[contains(@class,'admin__data-grid-header')]//div[contains(@class,'admin__filter-actions')]/button[2]"/> <element name="filterByTermName" type="input" selector="#agreementGrid_filter_name"/> + <element name="firstRow" type="block" selector=".data-grid>tbody>tr"/> <element name="firstRowConditionName" type="text" selector=".data-grid>tbody>tr>td.col-name"/> <element name="firstRowConditionId" type="text" selector=".data-grid>tbody>tr>td.col-id.col-agreement_id"/> <element name="successMessage" type="text" selector=".message-success"/> + <element name="allTermRows" type="block" selector="//table[@id='agreementGrid_table']//tbody//tr[not(contains(@class,'data-grid-tr-no-data'))]"/> + <element name="emptyGrid" type="block" selector="//table[@id='agreementGrid_table']//tbody//tr[contains(@class,'data-grid-tr-no-data')]"/> </section> </sections> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml index cb3e98949c622..e62148ad30a94 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml @@ -12,5 +12,6 @@ <element name="checkoutAgreementCheckbox" type="checkbox" selector="div.checkout-agreement.field.choice.required > input"/> <element name="checkoutAgreementButton" type="button" selector="div.checkout-agreements-block > div > div > div > label > button > span"/> <element name="checkoutAgreementErrorMessage" type="button" selector="div.checkout-agreement.field.choice.required > div.mage-error"/> + <element name="checkoutAgreementCheckboxcheck" type="checkbox" selector="//span[text()='{{agreementname}}']/../../../input[@type='checkbox']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml index ad39e8105e957..9720c3784d996 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml @@ -29,14 +29,13 @@ </before> <after> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeHtmlTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml index a90c3536ec744..f0c2bac759829 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml @@ -20,9 +20,7 @@ <group value="mtf_migrated"/> </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml index e74235dba19db..68104d4554685 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml @@ -32,10 +32,9 @@ <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{disabledTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml index 3eb1e9dd02c9d..2484efcda6fd5 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml @@ -33,7 +33,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> <deleteData createDataKey="createdCustomer" stepKey="deletedCustomer"/> @@ -41,10 +41,9 @@ <deleteData createDataKey="createdProduct2" stepKey="deletedProduct2"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml index 175d5eb621501..0e7074d1a1803 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14663"/> <group value="checkoutAgreements"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -32,14 +33,14 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> <deleteData createDataKey="createdProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> + <actionGroup ref="AdminOpenEditPageTermsConditionsByNameActionGroup" stepKey="openTermToDelete"> <argument name="termName" value="{{activeTextTerm.name}}"/> </actionGroup> <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml index 83ce4df697e46..53c2b8663bd58 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml index f9d60796d0424..e0fcd2643606b 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml @@ -32,10 +32,9 @@ <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml index 198a9fe3fc7b4..18f52b197b3fe 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml @@ -21,9 +21,7 @@ </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeHtmlTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml index f82840bc07c7d..1613bab85edba 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml @@ -18,11 +18,10 @@ <testCaseId value="MC-14666"/> <group value="checkoutAgreements"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{disabledHtmlTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml new file mode 100644 index 0000000000000..9b7670dc54bba --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontManualTermsAndConditionsTest"> + <annotations> + <features value="CheckoutAgreements"/> + <stories value="Verify that Manual Terms and Condition is still required to be accept even payment solution was changed"/> + <title value="Verify Terms and Conditions"/> + <description value="Verify that Manual Terms and Condition is still required to be accept even payment solution was changed"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4723"/> + </annotations> + <before> + <!--Create Category--> + <createData entity="_defaultCategory" stepKey="testCategory"/> + <!-- Create SimpleProductWithPrice100 --> + <createData entity="SimpleProduct_100" stepKey="simpleProductOne"> + <requiredEntity createDataKey="testCategory"/> + </createData> + <!-- Assign SimpleProductOne to Category --> + <createData entity="AssignProductToCategory" stepKey="assignSimpleProductOneToTestCategory"> + <requiredEntity createDataKey="testCategory"/> + <requiredEntity createDataKey="simpleProductOne"/> + </createData> + <!-- Enable Terms And Condition--> + <magentoCLI command="config:set checkout/options/enable_agreements 1" stepKey="setEnableTermsOnCheckout"/> + <!--Login As Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Open New Terms And Conditions Page--> + <actionGroup ref="AdminTermsConditionsOpenNewTermPageActionGroup" stepKey="openNewTerm"/> + <!-- Fill the Required Details--> + <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> + <argument name="term" value="newHtmlTerm"/> + </actionGroup> + <grabTextFrom selector="{{AdminNewTermFormSection.conditionName}}" stepKey="conditionName"/> + <!-- Save Details--> + <actionGroup ref="AdminTermsConditionsSaveTermActionGroup" stepKey="saveFilledTerm"/> + <!--Enable Cash On Delivery Method --> + <magentoCLI command="config:set {{CashOnDeliveryEnableConfigData.path}} {{CashOnDeliveryEnableConfigData.value}}" stepKey="enableCashOnDelivery"/> + </before> + <after> + <deleteData createDataKey="simpleProductOne" stepKey="deleteProduct"/> + <deleteData createDataKey="testCategory" stepKey="deleteTestCategory"/> + <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> + <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> + <magentoCLI command="config:set {{CashOnDeliveryDisabledConfigData.path}} {{CashOnDeliveryDisabledConfigData.value}}" stepKey="disabledCashOnDelivery"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--Go to product page--> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$simpleProductOne.custom_attributes[url_key]$"/> + </actionGroup> + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$simpleProductOne.name$"/> + </actionGroup> + <!-- Proceed to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinCart"/> + <!--Filling shipping information and click next--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + <argument name="customerVar" value="Simple_US_Customer_NY"/> + <argument name="customerAddressVar" value="US_Address_NY"/> + </actionGroup> + <!-- SelectCash On Delivery payment method --> + <click selector="{{StorefrontCheckoutPaymentMethodsSection.cashOnDelivery}}" stepKey="selectCashOnDeliveryMethod"/> + <!-- Verify Address is present--> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPage"> + <argument name="customerVar" value="Simple_US_Customer_NY" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + <!--Check-box with text for Terms and Condition is present--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckout"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckout"/> + <!--Click Place Order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <!-- Check "This is a required field." message is appeared under check-box--> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorTextInCheckout"/> + <!-- Select Check Money Order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!--Section for *CheckMoneyOrder* is opened--> + <seeElement selector ="{{AdminOrderFormPaymentSection.checkoutPaymentMethod('checkmo')}}" stepKey="checkMoneyOrderPageIsOpened"/> + <!--Check Section for *Cash On Delivery* is closed --> + <dontSeeElement selector ="{{AdminOrderFormPaymentSection.checkoutPaymentMethod('cashondelivery')}}" stepKey="cashOnDelivery"/> + <!--Check-box with text for Terms and Condition is presented--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckoutIsPresent"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckoutIsPresent"/> + <!-- Click PLace Order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderAgain"/> + <!--Check This is a required field." message is appeared under check-box --> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorMessage"/> + <!-- Check check-box for Terms and Condition--> + <selectOption selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="checkAgreement"/> + <!-- Select Cash On Delivery payment method Again--> + <click selector="{{StorefrontCheckoutPaymentMethodsSection.cashOnDelivery}}" stepKey="selectCashOnDeliveryMethodAgain"/> + <!-- Check Address is present--> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPageAgain"> + <argument name="customerVar" value="Simple_US_Customer_NY" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + <!--Check-box with text for Terms and Condition is presented--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckoutAgain"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckoutAgain"/> + <seeCheckboxIsChecked selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckboxcheck(newHtmlTerm.checkboxText)}}" stepKey="checkbox"/> + <!-- Click PLace Order Again--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="PlaceOrder"/> + <!--This is a required field." message is appeared under check-box --> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeAgainErrorTextInCheckoutBox"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php index 86976f6c912e8..897ce651146b8 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php @@ -100,7 +100,7 @@ public function getRowClickCallback() $js = ' function (grid, event) { var trElement = Event.findElement(event, "tr"); - var blockId = trElement.down("td").innerHTML.replace(/^\s+|\s+$/g,""); + var blockId = trElement.down("td").next().next().innerHTML.replace(/^\s+|\s+$/g,""); var blockTitle = trElement.down("td").next().innerHTML; ' . $chooserJsObject . diff --git a/app/code/Magento/Cms/Controller/Noroute/Index.php b/app/code/Magento/Cms/Controller/Noroute/Index.php index b30beae73dce1..6eeb80be375e4 100644 --- a/app/code/Magento/Cms/Controller/Noroute/Index.php +++ b/app/code/Magento/Cms/Controller/Noroute/Index.php @@ -4,17 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Noroute; +use Magento\Framework\Controller\Result\ForwardFactory; + /** * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Index extends \Magento\Framework\App\Action\Action { /** - * @var \Magento\Framework\Controller\Result\ForwardFactory + * @var ForwardFactory */ - protected $resultForwardFactory; + protected ForwardFactory $resultForwardFactory; /** * @param \Magento\Framework\App\Action\Context $context @@ -48,6 +52,7 @@ public function execute() if ($resultPage) { $resultPage->setStatusHeader(404, '1.1', 'Not Found'); $resultPage->setHeader('Status', '404 File not found'); + $resultPage->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0', true); return $resultPage; } else { /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ diff --git a/app/code/Magento/Cms/Model/Page/IdentityMap.php b/app/code/Magento/Cms/Model/Page/IdentityMap.php index 249010fbf90ce..ba26f0520c567 100644 --- a/app/code/Magento/Cms/Model/Page/IdentityMap.php +++ b/app/code/Magento/Cms/Model/Page/IdentityMap.php @@ -8,11 +8,12 @@ namespace Magento\Cms\Model\Page; use Magento\Cms\Model\Page; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Identity map of loaded pages. */ -class IdentityMap +class IdentityMap implements ResetAfterRequestInterface { /** * @var Page[] @@ -69,4 +70,12 @@ public function clear(): void { $this->pages = []; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->pages = []; + } } diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php new file mode 100644 index 0000000000000..c25a0b58c9c9d --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Get target Url from routePath and store code. + */ +class TargetUrlBuilder implements TargetUrlBuilderInterface +{ + /** + * @var UrlInterface + */ + private $frontendUrlBuilder; + + /** + * Initialize constructor + * + * @param UrlInterface $frontendUrlBuilder + */ + public function __construct(UrlInterface $frontendUrlBuilder) + { + $this->frontendUrlBuilder = $frontendUrlBuilder; + } + + /** + * Get target URL + * + * @param string $routePath + * @param string $store + * @return string + */ + public function process(string $routePath, string $store): string + { + return $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + } +} diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php new file mode 100644 index 0000000000000..2ac8d5d3379ea --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +/** + * Provides extension point to generate target url for url builder class + */ +interface TargetUrlBuilderInterface +{ + /** + * Get target url from the route and store code + * + * @param string $routePath + * @param string $store + * @return string + */ + public function process(string $routePath, string $store): string; +} diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php index f22367393030a..7d30908a2d9d6 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php @@ -19,15 +19,11 @@ class Collection extends AbstractCollection protected $_idFieldName = 'block_id'; /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'cms_block_collection'; /** - * Event object - * * @var string */ protected $_eventObject = 'block_collection'; diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php index 96886a995b1c9..6aafb010fb625 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php @@ -26,15 +26,11 @@ class Collection extends AbstractCollection protected $_previewFlag; /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'cms_page_collection'; /** - * Event object - * * @var string */ protected $_eventObject = 'page_collection'; diff --git a/app/code/Magento/Cms/README.md b/app/code/Magento/Cms/README.md index 7934f52cdf341..23e55f1b01a43 100644 --- a/app/code/Magento/Cms/README.md +++ b/app/code/Magento/Cms/README.md @@ -18,29 +18,33 @@ The module interacts with the following layout handles: The module interacts with the following layout handles: `view/adminhtml/layout` directory: - - `cms_block_edit.xml` - - `cms_block_index.xml` - - `cms_block_new.xml` - - `cms_page_edit.xml` - - `cms_page_index.xml` - - `cms_page_new.xml` - - `cms_wysiwyg_images_contents.xml` - - `cms_wysiwyg_images_index.xml` + + * `cms_block_edit.xml` + * `cms_block_index.xml` + * `cms_block_new.xml` + * `cms_page_edit.xml` + * `cms_page_index.xml` + * `cms_page_new.xml` + * `cms_wysiwyg_images_contents.xml` + * `cms_wysiwyg_images_index.xml` The module interacts with the following layout handles in the `view/frontend/layout` directory: - - `cms_index_defaultindex.xml` - - `cms_index_defaultnoroute.xml` - - `cms_index_index.xml` - - `cms_index_nocookies.xml` - - `cms_noroute_index.xml` - - `cms_page_view.xml` - - `default.xml` - - `print.xml` + + * `cms_index_defaultindex.xml` + * `cms_index_defaultnoroute.xml` + * `cms_index_index.xml` + * `cms_index_nocookies.xml` + * `cms_noroute_index.xml` + * `cms_page_view.xml` + * `default.xml` + * `print.xml` ### UI components + This module extends following ui components located in the `view/base/ui_component` directory: This module extends following ui components located in the `view/adminhtml/ui_component` directory: - - `cms_block_form.xml` - - `cms_block_listing.xml` - - `cms_page_form.xml` - - `cms_page_listing.xml` + + * `cms_block_form.xml` + * `cms_block_listing.xml` + * `cms_page_form.xml` + * `cms_page_listing.xml` diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml new file mode 100644 index 0000000000000..3054d6eb31414 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertCMSPageContentParamValueActionGroup"> + <annotations> + <description>Assert content param with value on CMS page.</description> + </annotations> + <arguments> + <argument name="param" type="string"/> + <argument name="value" type="string"/> + </arguments> + + <grabValueFrom selector="{{CmsNewPagePageActionsSection.content}}" stepKey="grabContent"/> + <assertStringContainsString stepKey="assertClass"> + <actualResult type="string">{$grabContent}</actualResult> + <expectedResult type="string">{{param}}="{{value}}"</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml new file mode 100644 index 0000000000000..7ff5cb3dabecd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.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="AdminClickSelectBlockActionGroup"> + <annotations> + <description>Click on Select Block button.</description> + </annotations> + + <waitForElementVisible selector="{{WidgetSection.BtnChooser}}" stepKey="waitForSelectBlockButtonVisible"/> + <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectBlockBtn"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml new file mode 100644 index 0000000000000..0afce4f1dc222 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.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="AdminSelectBlockOnGridActionGroup"> + <annotations> + <description>Selects block on grid and click insert widget button.</description> + </annotations> + <arguments> + <argument name="block"/> + </arguments> + + <click selector="{{WidgetSection.BlockPage(block.identifier)}}" stepKey="selectPreCreateBlock" /> + <waitForElementVisible selector="{{WidgetSection.InsertWidget}}" stepKey="waitForInsertWidgetBtn"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidgetBtn" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml index bb276b2adb0de..de70c5706360a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml @@ -16,5 +16,6 @@ <element name="mainContent" type="text" selector="#maincontent"/> <element name="footerTop" type="text" selector="footer.page-footer"/> <element name="title" type="text" selector="//div[@class='breadcrumbs']//ul/li[@class='item cms_page']"/> + <element name="widgetContentApostrophe" type="text" selector="//div[@class='widget block block-cms-link']//span[contains(text(),'{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml index 5be91f61e1e1e..c9ef757ca7477 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml @@ -48,5 +48,8 @@ <element name="ChooserName" type="input" selector="input[name='chooser_name']"/> <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> + <element name="URLKeySelectPage" type="input" selector="//aside[@role='dialog']//input[@name='chooser_identifier']"/> + <element name="SearchButtonSelectPage" type="button" selector="//aside[@role='dialog']//button[@title='Search']"/> + <element name="SearchResultSelectPage" type="text" selector="//aside[@role='dialog']//td[contains(@class,'col-url col-chooser_identifier') and contains(text(),'{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml new file mode 100644 index 0000000000000..0eb511beb2a05 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddBlockWidgetToCMSPageTest"> + <annotations> + <features value="Cms"/> + <stories value="Add block to page and check block id"/> + <title value="Add block to CMS page and check block id"/> + <description value="Add block to CMS page and check block_id in content"/> + <severity value="AVERAGE"/> + <group value="backend"/> + <group value="Cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <createData entity="_defaultBlock" stepKey="createPreReqBlock"> + <field key="identifier">block-id-777</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Navigate to Page in Admin --> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + + <!-- Insert block page --> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="insertWidgetToCmsPageContent"> + <argument name="widgetType" value="CMS Static Block"/> + </actionGroup> + <actionGroup ref="AdminClickSelectBlockActionGroup" stepKey="clickSelectBlockButton"/> + <actionGroup ref="searchBlockOnGridPage" stepKey="searchBlockOnGridPage"> + <argument name="Block" value="$$createPreReqBlock$$"/> + </actionGroup> + <actionGroup ref="AdminSelectBlockOnGridActionGroup" stepKey="selectBlockOnGrid"> + <argument name="block" value="$$createPreReqBlock$$"/> + </actionGroup> + + <!-- Assert block_id value in page content --> + <actionGroup ref="AdminAssertCMSPageContentParamValueActionGroup" stepKey="assertBlockId"> + <argument name="param" value="block_id"/> + <argument name="value" value="$$createPreReqBlock.identifier$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml new file mode 100644 index 0000000000000..6fd7acbdd2e98 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.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="AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest"> + <annotations> + <stories value="Create tax rule for grouped product in recently viewed widget"/> + <title value="Create tax rule for grouped product in recently viewed widget"/> + <description value="Create tax rule for grouped product in recently viewed widget"/> + <testCaseId value="AC-6282"/> + <severity value="CRITICAL"/> + <skip> + <issueId value="https://github.com/magento/magento2/issues/37322"/> + </skip> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + <!-- Create tax rate for TX --> + <createData entity="TaxRateTexas" stepKey="createTaxRateTX"/> + <!-- Create tax rule --> + <actionGroup ref="AdminCreateTaxRuleWithTwoTaxRatesActionGroup" stepKey="createTaxRule"> + <argument name="taxRate" value="$$createTaxRateTX$$"/> + <argument name="taxRate2" value="US_NY_Rate_1"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <magentoCLI command="config:set {{CustomDisplayProductPricesInCatalog.path}} {{CustomDisplayProductPricesInCatalog.value}}" stepKey="selectInclAndExlTax"/> + <magentoCLI command="cron:run --group=index" stepKey="runCronReindex"/> + <!-- Create customer --> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteProduct"/> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <!-- Delete tax rate for UK --> + <deleteData createDataKey="createTaxRateTX" stepKey="deleteTaxRateUK"/> + <!-- Delete customer --> + <magentoCLI command="config:set {{DisplayProductPricesInCatalog.path}} {{DisplayProductPricesInCatalog.value}}" stepKey="selectExlTax"/> + <magentoCron groups="index" stepKey="reindex"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE}}" stepKey="waitForTinyMCE"/> + <executeJS function="tinyMCE.activeEditor.setContent('Hello CMS Page!');" stepKey="executeJSFillContent"/> + <seeElement selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="seeWidgetIcon" /> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> + <!--see Insert Widget button disabled--> + <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> + <!--see Cancel button enabled--> + <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> + <!--Select "Widget Type"--> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Viewed Products" stepKey="selectRecentlyViewedProducts" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear" /> + <!--see Insert Widget button enabled--> + <see selector="{{WidgetSection.InsertWidgetBtnEnabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetEnabled" /> + <fillField selector="{{WidgetSection.PageSize}}" userInput="5" stepKey="fillNoOfProductDisplay" /> + <selectOption selector="{{WidgetSection.ProductAttribute}}" parameterArray="['Name','Image','Price','Learn More Link']" stepKey="selectSpecifiedOptions"/> + <selectOption selector="{{WidgetSection.ButtonToShow}}" userInput="Add to Cart" stepKey="selectBtnToShow" /> + <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Viewed Products Grid Template" stepKey="selectTemplate" /> + <actionGroup ref="AdminClickInsertWidgetActionGroup" stepKey="clickInsertWidget"/> + <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab" /> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{_defaultCmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="clickSavePage"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Navigate to the product --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <amOnPage url="$$createGroupedProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage" /> + <waitForPageLoad stepKey="waitForPage" /> + <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="wait5" /> + <!--see widget on Storefront--> + <see userInput="Hello CMS Page!" stepKey="seeContent"/> + <waitForPageLoad stepKey="wait6" /> + <waitForText userInput="$$createGroupedProduct.name$$" stepKey="waitForProductVisible" /> + <grabTextFrom selector="{{StoreFrontRecentlyViewedProductSection.ProductPrice}}" stepKey="grabRelatedProductPosition"/> + <assertStringContainsString stepKey="assertRelatedProductPrice"> + <actualResult type="const">$grabRelatedProductPosition</actualResult> + <expectedResult type="string">$133.30</expectedResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml index 0d483e21499fb..9ad4df9e52296 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml index c7b3f7f27e946..ca07a5cdd0bb5 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -32,6 +33,7 @@ </after> <amOnPage url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="navigateToPageGridWithFilters"/> <waitForPageLoad stepKey="waitForPageGrid"/> + <seeInCurrentUrl url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="assertUrl"/> <waitForText selector="{{CmsPagesPageActionsSection.pagesGridRowByTitle($$createPage.title$$)}}" userInput="$$createPage.title$$" stepKey="seePage"/> <seeInCurrentUrl url="admin/cms/page?filters" stepKey="seeAdminCMSPageFilters"/> <waitForElementVisible selector="{{CmsPagesPageActionsSection.activeFilter}}" stepKey="seeEnabledFilters"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml index 53bb2619075a7..b6165f5c59554 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="CMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml index 03a168adf4903..f324b5d1a413d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-89025"/> <group value="Cms"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml new file mode 100644 index 0000000000000..b06bb0341d517 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigureStoreInformationTest"> + <annotations> + <features value="Cms"/> + <stories value="able to configure store information data"/> + <title value="Admin Configure Store Information"/> + <description value="As a Merchant I want to be able to configure store information data"/> + <severity value="MAJOR"/> + <testCaseId value="AC-3963"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToBackend"/> + </before> + <after> + <actionGroup ref="AdminSetStoreInformationConfigurationActionGroup" stepKey="resetStoreInformationConfig"> + <argument name="storeName" value=""/> + <argument name="storeHoursOfOperation" value=""/> + <argument name="vatNumber" value=""/> + <argument name="telephone" value=""/> + <argument name="country" value=""/> + <argument name="state" value=""/> + <argument name="city" value=""/> + <argument name="postcode" value=""/> + <argument name="street" value=""/> + </actionGroup> + <actionGroup ref="DeletePageByUrlKeyActionGroup" stepKey="deletePage"> + <argument name="UrlKey" value="{{_defaultCmsPage.identifier}}"/> + </actionGroup> + <actionGroup ref="EuropeanCountriesSystemCheckBoxActionGroup" stepKey="checkSystemValueConfig"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Set StoreInformation configs data--> + <actionGroup ref="AdminSetStoreInformationConfigurationActionGroup" stepKey="setStoreInformationConfigData"> + <argument name="telephone" value="{{DE_Address_Berlin_Not_Default_Address.telephone}}"/> + <argument name="country" value="{{DE_Address_Berlin_Not_Default_Address.country_id}}"/> + <argument name="state" value="{{DE_Address_Berlin_Not_Default_Address.state}}"/> + <argument name="city" value="{{DE_Address_Berlin_Not_Default_Address.city}}"/> + <argument name="postcode" value="{{DE_Address_Berlin_Not_Default_Address.postcode}}"/> + <argument name="street" value="{{DE_Address_Berlin_Not_Default_Address.street[0]}}"/> + </actionGroup> + <magentoCLI command="config:set {{SetEuropeanUnionCountries.path}} {{SetEuropeanUnionCountries.value}}" stepKey="selectEUCountries"/> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <click selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="clickAddNewPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> + <actionGroup ref="SaveAndContinueEditCmsPageActionGroup" stepKey="saveAndContinueEditCmsPage"/> + <actionGroup ref="switchToPageBuilderStage" stepKey="switchToPageBuilderStage"/> + <actionGroup ref="dragContentTypeToStage" stepKey="dragRowToRootContainer"> + <argument name="contentType" value="PageBuilderRowContentType"/> + <argument name="containerTargetType" value="PageBuilderRootContainerContentType"/> + </actionGroup> + <actionGroup ref="expandPageBuilderPanelMenuSection" stepKey="expandPageBuilderPanelMenuSection"> + <argument name="contentType" value="PageBuilderTextContentType"/> + </actionGroup> + <actionGroup ref="dragContentTypeToStage" stepKey="dragToStage"> + <argument name="contentType" value="PageBuilderHtmlContentType"/> + </actionGroup> + <actionGroup ref="openPageBuilderEditPanel" stepKey="openEditMenuOnStage"> + <argument name="contentType" value="PageBuilderHtmlContentType"/> + </actionGroup> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableButton"/> + <waitForPageLoad stepKey="waitForPageToLoadForToInsertButtonForStoreName"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Store Name')}}" stepKey="selectDefaultVariable"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStoreName"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForStAds"/> + <waitForPageLoad stepKey="waitForPageToLoadToSelectInsertVariableButtonForStAds"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="againClickInsertVariableButtonForStAds"/> + <waitForPageLoad stepKey="againWaitForPageToLoadToSelectInsertVariableButtonForStAds"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Street Address')}}" stepKey="selectDefaultVariableForStAds"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStAds"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForStore"/> + <waitForPageLoad stepKey="waitForPageToLoadForToInsertButtonForStore"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="againClickInsertVariableForStore"/> + <waitForPageLoad stepKey="againWaitForPageToLoadToSelectInsertVariableButtonForStore"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / City')}}" stepKey="selectDefaultVariableForStore"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStore"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForCode"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForCode"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForCode"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / ZIP/Postal Code')}}" stepKey="selectDefaultVariableForCode"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableCode"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForState"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForState"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForState"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Region/State')}}" stepKey="selectDefaultVariableForState"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableForState"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForCountry"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForCountry"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForCountry"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Country')}}" stepKey="selectDefaultVariableForCountry"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableForCountry"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariable"/> + <waitForPageLoad stepKey="waitForLoad"/> + <actionGroup ref="saveEditPanelSettingsFullScreen" stepKey="saveEditPanelSettings"/> + <actionGroup ref="exitPageBuilderFullScreen" stepKey="exitPageBuilderFullScreen"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{_defaultCmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <actionGroup ref="SaveAndContinueEditCmsPageActionGroup" stepKey="saveAndContinueEditCmsPageAgain"/> + <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPageRefresh"/> + <see userInput="New Store InformationAugsburger Strabe 41Berlin10789BerlinGermany" stepKey="seeCustomData" /> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml index 7d3946ea86c92..51f62376439ff 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14129"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml index 0a7d794b6d17a..900b3ec4341b8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml index 4ac851b8b2a1e..eabcdf9e84c92 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="newDefaultCategory"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml index 6f9861cd18dcf..99b06033763a3 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="newDefaultCategory"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml index 9e83b02d9184e..99cff6935ec11 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to delete CMS block from grid"/> <group value="Cms"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultBlock" stepKey="createCMSBlock"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 245b1486058b8..0fffdf35a0ee4 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="cms"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleCmsPage" stepKey="createFirstCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml index 33e614e566c29..c1a53e0f4ff8c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-93978"/> <group value="Cms"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_longContentCmsPage" stepKey="createPreReqCMSPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml index 8c15d6f4c24ce..448f757bc28cb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-96388"/> <useCaseId value="MAGETWO-57337"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php index 665b79fdf48be..6f1998ac98023 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php @@ -29,17 +29,17 @@ class IndexTest extends TestCase /** * @var Index */ - protected $_controller; + protected Index $_controller; /** * @var MockObject */ - protected $_cmsHelperMock; + protected MockObject $_cmsHelperMock; /** * @var MockObject */ - protected $_requestMock; + protected MockObject $_requestMock; /** * @var ForwardFactory|MockObject @@ -119,10 +119,14 @@ public function testExecuteResultPage(): void ->method('setStatusHeader') ->with(404, '1.1', 'Not Found') ->willReturn($this->resultPageMock); + $this->resultPageMock ->method('setHeader') - ->with('Status', '404 File not found') - ->willReturn($this->resultPageMock); + ->withConsecutive( + ['Status', '404 File not found'], + ['Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'] + )->willReturn($this->resultPageMock); + $this->_cmsHelperMock->expects( $this->once() )->method( diff --git a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php index bc291b865c6ef..6193b1f968713 100644 --- a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php +++ b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php @@ -7,6 +7,7 @@ namespace Magento\Cms\Test\Unit\ViewModel\Page\Grid; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; use Magento\Cms\ViewModel\Page\Grid\UrlBuilder; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; @@ -42,23 +43,31 @@ class UrlBuilderTest extends TestCase */ private $storeManagerMock; + /** + * @var TargetUrlBuilderInterface + */ + private $getTargetUrlMock; + /** * Set Up */ protected function setUp(): void { $this->frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class) - ->setMethods(['getUrl', 'setScope']) + ->onlyMethods(['getUrl', 'setScope']) ->getMockForAbstractClass(); $this->urlEncoderMock = $this->getMockForAbstractClass(EncoderInterface::class); $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->getTargetUrlMock = $this->getMockBuilder(TargetUrlBuilderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->viewModel = new UrlBuilder( $this->frontendUrlBuilderMock, $this->urlEncoderMock, - $this->storeManagerMock + $this->storeManagerMock, + $this->getTargetUrlMock ); } @@ -109,54 +118,34 @@ public function nonScopedUrlsDataProvider(): array /** * Testing url builder with a scope provided * - * @dataProvider scopedUrlsDataProvider + * @param array $routePaths + * @param array $expectedUrls * - * @param string $storeCode - * @param string $defaultStoreCode - * @param array $urlParams - * @param string $scope + * @dataProvider scopedUrlsDataProvider */ public function testScopedUrlBuilder( - string $storeCode, - string $defaultStoreCode, - array $urlParams, - string $scope = 'store' + array $routePaths, + array $expectedUrls ) { /** @var StoreInterface|MockObject $storeMock */ $storeMock = $this->getMockForAbstractClass(StoreInterface::class); $storeMock->expects($this->any()) ->method('getCode') - ->willReturn($defaultStoreCode); + ->willReturn('en'); $this->storeManagerMock->expects($this->once()) ->method('getDefaultStoreView') ->willReturn($storeMock); - + $this->getTargetUrlMock->expects($this->any()) + ->method('process') + ->withConsecutive([$routePaths[0], 'en'], [$routePaths[1], 'en']) + ->willReturnOnConsecutiveCalls($routePaths[0], $routePaths[1]); $this->frontendUrlBuilderMock->expects($this->any()) ->method('getUrl') - ->withConsecutive( - [ - 'test/index', - [ - '_current' => false, - '_nosid' => true, - '_query' => [ - StoreManagerInterface::PARAM_NAME => $storeCode - ] - ] - ], - [ - 'stores/store/switch', - $urlParams - ] - ) - ->willReturnOnConsecutiveCalls( - 'http://domain.com/test', - 'http://domain.com/test/index' - ); - - $result = $this->viewModel->getUrl('test/index', $scope, $storeCode); - - $this->assertSame('http://domain.com/test/index', $result); + ->willReturnOnConsecutiveCalls($expectedUrls[0], $expectedUrls[1]); + + $result = $this->viewModel->getUrl($routePaths[0], 'store', 'en'); + + $this->assertSame($expectedUrls[0], $result); } /** @@ -166,28 +155,14 @@ public function testScopedUrlBuilder( */ public function scopedUrlsDataProvider(): array { - $enStoreCode = 'en'; - $frStoreCode = 'fr'; - $scopedDefaultUrlParams = $defaultUrlParams = [ - '_current' => false, - '_nosid' => true, - '_query' => [ - '___store' => $enStoreCode, - 'uenc' => null, - ] - ]; - $scopedDefaultUrlParams['_query']['___from_store'] = $frStoreCode; - return [ [ - $enStoreCode, - $enStoreCode, - $defaultUrlParams, + ['test1/index1', 'stores/store/switch'], + ['http://domain.com/test1', 'http://domain.com/test1/index1'] ], [ - $enStoreCode, - $frStoreCode, - $scopedDefaultUrlParams + ['fr/test2/index2', 'stores/store/switch'], + ['http://domain.com/fr/test2', 'http://domain.com/fr/test2/index2'] ] ]; } diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php index 15b9fe408d228..312496f2683f6 100644 --- a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php +++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php @@ -7,8 +7,11 @@ namespace Magento\Cms\ViewModel\Page\Grid; -use Magento\Framework\Url\EncoderInterface; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,7 +20,7 @@ class UrlBuilder { /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ private $frontendUrlBuilder; @@ -32,18 +35,27 @@ class UrlBuilder private $storeManager; /** - * @param \Magento\Framework\UrlInterface $frontendUrlBuilder + * @var TargetUrlBuilderInterface + */ + private $getTargetUrl; + + /** + * @param UrlInterface $frontendUrlBuilder * @param EncoderInterface $urlEncoder * @param StoreManagerInterface $storeManager + * @param TargetUrlBuilderInterface|null $getTargetUrl */ public function __construct( - \Magento\Framework\UrlInterface $frontendUrlBuilder, + UrlInterface $frontendUrlBuilder, EncoderInterface $urlEncoder, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?TargetUrlBuilderInterface $getTargetUrl = null ) { $this->frontendUrlBuilder = $frontendUrlBuilder; $this->urlEncoder = $urlEncoder; $this->storeManager = $storeManager; + $this->getTargetUrl = $getTargetUrl ?: + ObjectManager::getInstance()->get(TargetUrlBuilderInterface::class); } /** @@ -58,16 +70,7 @@ public function getUrl($routePath, $scope, $store) { if ($scope) { $this->frontendUrlBuilder->setScope($scope); - $targetUrl = $this->frontendUrlBuilder->getUrl( - $routePath, - [ - '_current' => false, - '_nosid' => true, - '_query' => [ - StoreManagerInterface::PARAM_NAME => $store - ] - ] - ); + $targetUrl = $this->getTargetUrl->process($routePath, $store); $href = $this->frontendUrlBuilder->getUrl( 'stores/store/switch', [ diff --git a/app/code/Magento/Cms/etc/adminhtml/di.xml b/app/code/Magento/Cms/etc/adminhtml/di.xml index e2ef86b7f650b..aa1b812561a2d 100644 --- a/app/code/Magento/Cms/etc/adminhtml/di.xml +++ b/app/code/Magento/Cms/etc/adminhtml/di.xml @@ -62,4 +62,10 @@ <argument name="variableConfig" xsi:type="object">Magento\Variable\Model\Variable\Config\Proxy</argument> </arguments> </type> + <preference for="Magento\Cms\Model\Page\TargetUrlBuilderInterface" type="Magento\Cms\Model\Page\TargetUrlBuilder"/> + <type name="Magento\Cms\Model\Page\TargetUrlBuilder"> + <arguments> + <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..d4cce9f7d58f7 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Model\Block; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Block::CACHE_TAG; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + $ids = []; + $items = $resolvedData['items'] ?? []; + foreach ($items as $item) { + if (is_array($item) && !empty($item[BlockInterface::BLOCK_ID])) { + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::BLOCK_ID]); + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::IDENTIFIER]); + } + } + + return $ids; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..1a48504dac22e --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved CMS page for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Page::CACHE_TAG; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($resolvedData[PageInterface::PAGE_ID]) ? + [] : [sprintf('%s_%s', $this->cacheTag, $resolvedData[PageInterface::PAGE_ID])]; + } +} diff --git a/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php new file mode 100644 index 0000000000000..79028dde33468 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php @@ -0,0 +1,252 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Test\Integration\Model\Resolver; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; +use Magento\Framework\App\Cache\Type\FrontendPool; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\GraphQlResolverCache\Model\Plugin\Resolver\Cache as ResolverResultCachePlugin; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test GraphQl Resolver cache saves and loads properly + * @magentoAppArea graphql + */ +class PageTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var GraphQlRequest + */ + private $graphQlRequest; + + /** + * @var ResolverResultCachePlugin + */ + private $originalResolverResultCachePlugin; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var PageRepository + */ + private $pageRepository; + + /** + * @var CacheStateInterface + */ + private $cacheState; + + /** + * @var bool + */ + private $originalCacheStateEnabledStatus; + + protected function setUp(): void + { + $this->objectManager = $objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $objectManager->create(GraphQlRequest::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->pageRepository = $objectManager->get(PageRepository::class); + $this->originalResolverResultCachePlugin = $objectManager->get(ResolverResultCachePlugin::class); + + $this->cacheState = $objectManager->get(CacheStateInterface::class); + $this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER); + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true); + } + + protected function tearDown(): void + { + $objectManager = $this->objectManager; + + // reset to original resolver plugin + $objectManager->addSharedInstance($this->originalResolverResultCachePlugin, ResolverResultCachePlugin::class); + + // clean graphql resolver cache and reset to original enablement status + $objectManager->get(GraphQlResolverCache::class)->clean(); + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, $this->originalCacheStateEnabledStatus); + } + + /** + * Test that result can be loaded continuously after saving once when passing the same arguments + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testResultIsLoadedMultipleTimesAfterOnlyBeingSavedOnce() + { + $objectManager = $this->objectManager; + $page = $this->getPageByTitle('Page with 1column layout'); + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy calls load at least once for the same CMS page query + $cacheProxy + ->expects($this->atLeastOnce()) + ->method('load'); + + // assert save is called at most once for the same CMS page query + $cacheProxy + ->expects($this->once()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = $this->getQuery($page->getIdentifier()); + + // send request and assert save is called + $this->graphQlRequest->send($query); + + // send again and assert save is not called (i.e. result is loaded from resolver cache) + $this->graphQlRequest->send($query); + + // send again with whitespace appended and assert save is not called (i.e. result is loaded from resolver cache) + $this->graphQlRequest->send($query . ' '); + + // send again with a different field and assert save is not called (i.e. result is loaded from resolver cache) + $differentQuery = $this->getQuery($page->getIdentifier(), ['meta_title']); + $this->graphQlRequest->send($differentQuery); + } + + /** + * Test that resolver plugin does not call GraphQlResolverCache's save or load methods when it is disabled + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testNeitherSaveNorLoadAreCalledWhenResolverCacheIsDisabled() + { + $objectManager = $this->objectManager; + $page = $this->getPageByTitle('Page with 1column layout'); + + // disable graphql resolver cache + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, false); + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy never calls load + $cacheProxy + ->expects($this->never()) + ->method('load'); + + // assert save is also never called + $cacheProxy + ->expects($this->never()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = $this->getQuery($page->getIdentifier()); + + // send request multiple times and assert neither save nor load are called + $this->graphQlRequest->send($query); + $this->graphQlRequest->send($query); + } + + public function testSaveIsNeverCalledWhenMissingRequiredArgumentInQuery() + { + $objectManager = $this->objectManager; + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy never calls save + $cacheProxy + ->expects($this->never()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = <<<QUERY +{ + cmsPage { + title + } +} +QUERY; + + // send request multiple times and assert save is never called + $this->graphQlRequest->send($query); + $this->graphQlRequest->send($query); + } + + private function getQuery(string $identifier, array $fields = ['title']): string + { + $fields = implode(PHP_EOL, $fields); + + return <<<QUERY +{ + cmsPage(identifier: "$identifier") { + $fields + } +} +QUERY; + } + + private function getPageByTitle(string $title): PageInterface + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('title', $title) + ->create(); + + $pages = $this->pageRepository->getList($searchCriteria)->getItems(); + + /** @var PageInterface $page */ + $page = reset($pages); + + return $page; + } +} diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json index 07b7261823d92..ea6e6152cacca 100644 --- a/app/code/Magento/CmsGraphQl/composer.json +++ b/app/code/Magento/CmsGraphQl/composer.json @@ -7,7 +7,8 @@ "magento/framework": "*", "magento/module-cms": "*", "magento/module-widget": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "suggest": { "magento/module-graph-ql": "*", diff --git a/app/code/Magento/CmsGraphQl/etc/di.xml b/app/code/Magento/CmsGraphQl/etc/di.xml new file mode 100644 index 0000000000000..86efef7b2f960 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/etc/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver"> + <arguments> + <argument name="invalidatableObjectTypes" xsi:type="array"> + <item name="Magento\Cms\Api\Data\PageInterface" xsi:type="string"> + Magento\Cms\Api\Data\PageInterface + </item> + <item name="Magento\Cms\Api\Data\BlockInterface" xsi:type="string"> + Magento\Cms\Api\Data\BlockInterface + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml index 78c1071d8e07c..4fd07a377f7e6 100644 --- a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml @@ -18,4 +18,16 @@ </argument> </arguments> </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider"> + <arguments> + <argument name="cacheableResolverClassNameIdentityMap" xsi:type="array"> + <item name="Magento\CmsGraphQl\Model\Resolver\Page" xsi:type="string"> + Magento\CmsGraphQl\Model\Resolver\Page\ResolverCacheIdentity + </item> + <item name="Magento\CmsGraphQl\Model\Resolver\Blocks" xsi:type="string"> + Magento\CmsGraphQl\Model\Resolver\Block\ResolverCacheIdentity + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CmsGraphQl/etc/module.xml b/app/code/Magento/CmsGraphQl/etc/module.xml index 4fca42430d166..535374716eb71 100644 --- a/app/code/Magento/CmsGraphQl/etc/module.xml +++ b/app/code/Magento/CmsGraphQl/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_CmsGraphQl"> <sequence> <module name="Magento_GraphQl"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php new file mode 100644 index 0000000000000..f862e4ca60900 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Model\Page; + +use Magento\Cms\Model\Page; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Get target Url from routePath and store code. + */ +class TargetUrlBuilder implements TargetUrlBuilderInterface +{ + /** + * @var UrlInterface + */ + private $frontendUrlBuilder; + + /** + * @var Page + */ + private $cmsPage; + + /** + * @var UrlFinderInterface + */ + private $urlFinder; + + /** + * @var CmsPageUrlPathGenerator + */ + private $cmsPageUrlPathGenerator; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Initialize constructor + * + * @param UrlInterface $frontendUrlBuilder + * @param StoreManagerInterface $storeManager + * @param Page $cmsPage + * @param UrlFinderInterface $urlFinder + * @param CmsPageUrlPathGenerator $cmsPageUrlPathGenerator + */ + public function __construct( + UrlInterface $frontendUrlBuilder, + StoreManagerInterface $storeManager, + Page $cmsPage, + UrlFinderInterface $urlFinder, + CmsPageUrlPathGenerator $cmsPageUrlPathGenerator + ) { + $this->frontendUrlBuilder = $frontendUrlBuilder; + $this->storeManager = $storeManager; + $this->cmsPage = $cmsPage; + $this->urlFinder = $urlFinder; + $this->cmsPageUrlPathGenerator = $cmsPageUrlPathGenerator; + } + + /** + * Get target URL + * + * @param string $routePath + * @param string $store + * @return string + * @throws NoSuchEntityException + */ + public function process(string $routePath, string $store): string + { + $storeId = $this->storeManager->getStore($store)->getId(); + $pageId = $this->cmsPage->checkIdentifier($routePath, $storeId); + $currentUrlRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $routePath, + UrlRewrite::STORE_ID => $storeId, + ] + ); + $existingUrlRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $routePath + ] + ); + if ($currentUrlRewrite === null && $existingUrlRewrite !== null && !empty($pageId)) { + $cmsPage = $this->cmsPage->load($pageId); + $routePath = $this->cmsPageUrlPathGenerator->getCanonicalUrlPath($cmsPage); + } + return $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + } +} diff --git a/app/code/Magento/CmsUrlRewrite/README.md b/app/code/Magento/CmsUrlRewrite/README.md index 1f1b1ca782532..a1c20e3daefb0 100644 --- a/app/code/Magento/CmsUrlRewrite/README.md +++ b/app/code/Magento/CmsUrlRewrite/README.md @@ -1,6 +1,6 @@ ## Overview - -The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module. + +The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module. The module adds and removes URL rewrite rules as CMS pages are added or removed by a user. -The rules can be edited by an admin user as any other URL rewrite rule. +The rules can be edited by an admin user as any other URL rewrite rule. diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php new file mode 100644 index 0000000000000..940775764c62f --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php @@ -0,0 +1,176 @@ +<?php +/*** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Unit\Model\Page; + +use Magento\Cms\Model\Page; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class TargetUrlBuilderTest + * + * Testing the target url process successfully from the route path + */ +class TargetUrlBuilderTest extends TestCase +{ + /** + * @var TargetUrlBuilder + */ + private $viewModel; + + /** + * @var UrlInterface|MockObject + */ + private $frontendUrlBuilderMock; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var Page|MockObject + */ + private $cmsPageMock; + + /** + * @var CmsPageUrlPathGenerator|MockObject + */ + private $cmsPageUrlPathGeneratorMock; + + /** + * @var UrlFinderInterface|MockObject + */ + private $urlFinderMock; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class) + ->onlyMethods(['getUrl', 'setScope']) + ->getMockForAbstractClass(); + $this->cmsPageMock = $this->getMockBuilder(Page::class) + ->onlyMethods(['checkIdentifier']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->cmsPageUrlPathGeneratorMock = $this->getMockBuilder(CmsPageUrlPathGenerator::class) + ->onlyMethods(['getCanonicalUrlPath']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->urlFinderMock = $this->getMockBuilder(UrlFinderInterface::class) + ->onlyMethods(['findOneByData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->viewModel = new TargetUrlBuilder( + $this->frontendUrlBuilderMock, + $this->storeManagerMock, + $this->cmsPageMock, + $this->urlFinderMock, + $this->cmsPageUrlPathGeneratorMock + ); + } + + /** + * Testing getTargetUrl with a scope provided + * + * @dataProvider scopedUrlsDataProvider + * + * @param array $urlParams + * @param string $storeId + * @throws NoSuchEntityException + */ + public function testGetTargetUrl(array $urlParams, string $storeId): void + { + /** @var StoreInterface|MockObject $storeMock */ + $storeMock = $this->getMockForAbstractClass(StoreInterface::class); + $storeMock->expects($this->any()) + ->method('getId') + ->willReturn($storeId); + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $this->cmsPageMock->expects($this->any()) + ->method('checkIdentifier') + ->willReturn("1"); + $this->cmsPageUrlPathGeneratorMock->expects($this->any()) + ->method('getCanonicalUrlPath') + ->with($this->cmsPageMock) + ->willReturn('test/index'); + $this->urlFinderMock->expects($this->any()) + ->method('findOneByData') + ->willReturn('test/index'); + $this->frontendUrlBuilderMock->expects($this->any()) + ->method('getUrl') + ->withConsecutive( + [ + 'test/index', + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $storeId + ] + ] + ], + [ + 'stores/store/switch', + $urlParams + ] + ) + ->willReturnOnConsecutiveCalls( + 'http://domain.com/test', + 'http://domain.com/test/index' + ); + + $result = $this->viewModel->process('test/index', $storeId); + + $this->assertSame('http://domain.com/test', $result); + } + + /** + * Providing a scoped urls + * + * @return array + */ + public function scopedUrlsDataProvider(): array + { + $enStoreCode = 'en'; + $defaultUrlParams = [ + '_current' => false, + '_nosid' => true, + '_query' => [ + '___store' => $enStoreCode, + 'uenc' => null, + ] + ]; + + return [ + [ + $defaultUrlParams, + "1" + ], + [ + $defaultUrlParams, + "2" + ] + ]; + } +} diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml index c6b0e4b05f16b..b0839b233f8e9 100644 --- a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml +++ b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml @@ -9,4 +9,10 @@ <type name="Magento\Store\Model\ResourceModel\Store"> <plugin name="update_cms_url_rewrites_after_store_save" type="Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View"/> </type> + <preference for="Magento\Cms\Model\Page\TargetUrlBuilderInterface" type="Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder"/> + <type name="Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder"> + <arguments> + <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CompareListGraphQl/README.md b/app/code/Magento/CompareListGraphQl/README.md index ed1c38ab33a3b..92215c13a6792 100644 --- a/app/code/Magento/CompareListGraphQl/README.md +++ b/app/code/Magento/CompareListGraphQl/README.md @@ -1,4 +1,3 @@ # CompareListGraphQl module The CompareListGraphQl module is designed to implement compare product functionality. - diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 3b2c85b5d71d3..66d680127f538 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -6,22 +6,23 @@ namespace Magento\Config\App\Config\Type; +use Magento\Config\App\Config\Type\System\Reader; +use Magento\Framework\App\Cache\StateInterface; +use Magento\Framework\App\Cache\Type\Config; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Config\ConfigTypeInterface; use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\App\ObjectManager; -use Magento\Config\App\Config\Type\System\Reader; use Magento\Framework\App\ScopeInterface; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; -use Magento\Framework\Encryption\Encryptor; use Magento\Store\Model\ScopeInterface as StoreScope; -use Magento\Framework\App\Cache\StateInterface; -use Magento\Framework\App\Cache\Type\Config; +use Psr\Log\LoggerInterface; /** * System configuration type @@ -104,6 +105,10 @@ class System implements ConfigTypeInterface */ private $cacheState; + /** + * @var LoggerInterface + */ + private $logger; /** * System constructor. * @param ConfigSourceInterface $source @@ -119,6 +124,7 @@ class System implements ConfigTypeInterface * @param LockManagerInterface|null $locker * @param LockGuardedCacheLoader|null $lockQuery * @param StateInterface|null $cacheState + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -135,7 +141,8 @@ public function __construct( Encryptor $encryptor = null, LockManagerInterface $locker = null, LockGuardedCacheLoader $lockQuery = null, - StateInterface $cacheState = null + StateInterface $cacheState = null, + LoggerInterface $logger = null ) { $this->postProcessor = $postProcessor; $this->cache = $cache; @@ -148,6 +155,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); $this->cacheState = $cacheState ?: ObjectManager::getInstance()->get(StateInterface::class); + $this->logger = $logger + ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -265,7 +274,12 @@ private function loadDefaultScopeData() $cachedData = $this->cache->load($this->configType . '_' . $scopeType); $scopeData = false; if ($cachedData !== false) { - $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + try { + $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + } catch (\InvalidArgumentException $e) { + $this->logger->warning($e->getMessage()); + $scopeData = false; + } } return $scopeData; }; @@ -292,11 +306,13 @@ private function loadScopeData($scopeType, $scopeId) } $loadAction = function () use ($scopeType, $scopeId) { + /* Note: configType . '_scopes' needs to be loaded first to avoid race condition where cache finishes + saving after configType . '_' . $scopeType . '_' . $scopeId but before configType . '_scopes'. */ + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); $scopeData = false; if ($cachedData === false) { if ($this->availableDataScopes === null) { - $cachedScopeData = $this->cache->load($this->configType . '_scopes'); if ($cachedScopeData !== false) { $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); @@ -437,18 +453,113 @@ private function readData(): array */ public function clean() { - $this->data = []; $cleanAction = function () { - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $this->cacheData($this->readData()); // Note: If cache is enabled, pre-load the new config data. }; + $this->data = []; + if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { + // Note: If cache is disabled, we still clean cache in case it will be enabled later + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + return; + } + $this->lockQuery->lockedCleanData(self::$lockName, $cleanAction); + } + + /** + * Prepares data for cache by serializing and encrypting them + * + * Prepares data per scope to avoid reading data for all scopes on every request + * + * @param array $data + * @return array + */ + private function prepareDataForCache(array $data) :array + { + $dataToSave = []; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)), + $this->configType, + [System::CACHE_TAG] + ]; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])), + $this->configType . '_default', + [System::CACHE_TAG] + ]; + $scopes = []; + foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) { + foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { + $scopes[$curScopeType][$curScopeId] = 1; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)), + $this->configType . '_' . $curScopeType . '_' . $curScopeId, + [System::CACHE_TAG] + ]; + } + } + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)), + $this->configType . '_scopes', + [System::CACHE_TAG] + ]; + return $dataToSave; + } + /** + * Cache prepared configuration data. + * + * Takes data prepared by prepareDataForCache + * + * @param array $dataToSave + * @return void + */ + private function cachePreparedData(array $dataToSave) : void + { + foreach ($dataToSave as $datumToSave) { + $this->cache->save($datumToSave[0], $datumToSave[1], $datumToSave[2]); + } + } + + /** + * Gets configuration then cleans and warms it while locked + * + * This is to reduce the lock time after flushing config cache. + * + * @param callable $cleaner + * @return void + */ + public function cleanAndWarmDefaultScopeData(callable $cleaner) + { if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { - return $cleanAction(); + $cleaner(); + return; } + $loadAction = function () { + return false; + }; + $dataCollector = function () use ($cleaner) { + /* Note: call to readData() needs to be inside lock to avoid race conditions such as multiple + saves at the same time. */ + $newData = $this->readData(); + $preparedData = $this->prepareDataForCache($newData); + unset($newData); + $cleaner(); // Note: This is where other readers start waiting for us to finish saving cache. + return $preparedData; + }; + $dataSaver = function (array $preparedData) { + $this->cachePreparedData($preparedData); + }; + $this->lockQuery->lockedLoadData(self::$lockName, $loadAction, $dataCollector, $dataSaver); + } - $this->lockQuery->lockedCleanData( - self::$lockName, - $cleanAction - ); + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; } } diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index 2465eecec71dc..ed60f63717dac 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -26,7 +26,7 @@ class ValueProcessor /** * Placeholder for the output of sensitive data. */ - const SAFE_PLACEHOLDER = '******'; + public const SAFE_PLACEHOLDER = '******'; /** * System configuration structure factory. @@ -56,6 +56,9 @@ class ValueProcessor */ private $jsonSerializer; + /** @var Structure */ + private $configStructure; + /** * @param ScopeInterface $scope The object for managing configuration scope * @param StructureFactory $structureFactory The system configuration structure factory. @@ -87,11 +90,7 @@ public function __construct( */ public function process($scope, $scopeCode, $value, $path) { - $areaScope = $this->scope->getCurrentScope(); - $this->scope->setCurrentScope(Area::AREA_ADMINHTML); - /** @var Structure $configStructure */ - $configStructure = $this->configStructureFactory->create(); - $this->scope->setCurrentScope($areaScope); + $configStructure = $this->getConfigStructure(); /** @var Field $field */ $field = $configStructure->getElementByConfigPath($path); @@ -118,4 +117,21 @@ public function process($scope, $scopeCode, $value, $path) */ return is_array($processedValue) ? $this->jsonSerializer->serialize($processedValue) : $processedValue; } + + /** + * Retrieve config structure + * + * @return Structure + */ + private function getConfigStructure(): Structure + { + if (empty($this->configStructure)) { + $areaScope = $this->scope->getCurrentScope(); + $this->scope->setCurrentScope(Area::AREA_ADMINHTML); + /** @var Structure $configStructure */ + $this->configStructure = $this->configStructureFactory->create(); + $this->scope->setCurrentScope($areaScope); + } + return $this->configStructure; + } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index f5188d7a419b8..2ba52091161f7 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -182,6 +182,10 @@ public function save() return $this; } + /** + * Reload config to make sure config data is consistent with the database at this point. + */ + $this->_appConfig->reinit(); $oldConfig = $this->_getConfig(true); /** @var \Magento\Framework\DB\Transaction $deleteTransaction */ diff --git a/app/code/Magento/Config/Model/Config/Loader.php b/app/code/Magento/Config/Model/Config/Loader.php index 625c3cf2f41fe..fa48abcc6d26a 100644 --- a/app/code/Magento/Config/Model/Config/Loader.php +++ b/app/code/Magento/Config/Model/Config/Loader.php @@ -4,15 +4,14 @@ * See COPYING.txt for license details. */ -/** - * System configuration loader - */ namespace Magento\Config\Model\Config; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\ObjectManager; + /** - * Class which can read config by paths + * System configuration loader - Class which can read config by paths * - * @package Magento\Config\Model\Config * @api * @since 100.0.2 */ @@ -22,15 +21,26 @@ class Loader * Config data factory * * @var \Magento\Framework\App\Config\ValueFactory + * @deprecated + * @see $collectionFactory */ protected $_configValueFactory; + /** + * @var CollectionFactory + */ + private $collectionFactory; + /** * @param \Magento\Framework\App\Config\ValueFactory $configValueFactory + * @param ?CollectionFactory $collectionFactory */ - public function __construct(\Magento\Framework\App\Config\ValueFactory $configValueFactory) - { + public function __construct( + \Magento\Framework\App\Config\ValueFactory $configValueFactory, + CollectionFactory $collectionFactory = null + ) { $this->_configValueFactory = $configValueFactory; + $this->collectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get(CollectionFactory::class); } /** @@ -44,9 +54,8 @@ public function __construct(\Magento\Framework\App\Config\ValueFactory $configVa */ public function getConfigByPath($path, $scope, $scopeId, $full = true) { - $configDataCollection = $this->_configValueFactory->create(); - $configDataCollection = $configDataCollection->getCollection()->addScopeFilter($scope, $scopeId, $path); - + $configDataCollection = $this->collectionFactory->create(); + $configDataCollection->addScopeFilter($scope, $scopeId, $path); $config = []; $configDataCollection->load(); foreach ($configDataCollection->getItems() as $data) { diff --git a/app/code/Magento/Config/Model/ResourceModel/Config.php b/app/code/Magento/Config/Model/ResourceModel/Config.php index 594a9df719daa..79805f288beb2 100644 --- a/app/code/Magento/Config/Model/ResourceModel/Config.php +++ b/app/code/Magento/Config/Model/ResourceModel/Config.php @@ -6,6 +6,7 @@ namespace Magento\Config\Model\ResourceModel; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; /** * Core Resource Resource Model @@ -17,14 +18,23 @@ class Config extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements \Magento\Framework\App\Config\ConfigResource\ConfigInterface { + /** + * @var PoisonPillPutInterface + */ + private $pillPut; + /** * Define main table * + * @param PoisonPillPutInterface|null $pillPut * @return void */ - protected function _construct() - { + protected function _construct( + PoisonPillPutInterface $pillPut = null + ) { $this->_init('core_config_data', 'config_id'); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PoisonPillPutInterface::class); } /** @@ -61,6 +71,7 @@ public function saveConfig($path, $value, $scope = ScopeConfigInterface::SCOPE_T } else { $connection->insert($this->getMainTable(), $newData); } + $this->pillPut->put(); return $this; } @@ -83,6 +94,7 @@ public function deleteConfig($path, $scope = ScopeConfigInterface::SCOPE_TYPE_DE $connection->quoteInto('scope_id = ?', $scopeId) ] ); + $this->pillPut->put(); return $this; } } diff --git a/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php new file mode 100644 index 0000000000000..d82063f7c86c7 --- /dev/null +++ b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Plugin\Framework\App\Cache\TypeList; + +use Magento\Config\App\Config\Type\System; +use Magento\Framework\App\Cache\Type\Config as TypeConfig; +use Magento\Framework\App\Cache\TypeList; + +/** + * Plugin that for warms config cache when config cache is cleaned. + * This is to reduce the lock time after flushing config cache. + */ +class WarmConfigCache +{ + /** + * @var System + */ + private $system; + + /** + * @param System $system + */ + public function __construct(System $system) + { + $this->system = $system; + } + + /** + * Around plugin for cache's clean type method + * + * @param TypeList $subject + * @param callable $proceed + * @param string $typeCode + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundCleanType(TypeList $subject, callable $proceed, $typeCode) + { + if (TypeConfig::TYPE_IDENTIFIER !== $typeCode) { + return $proceed($typeCode); + } + $cleaner = function () use ($proceed, $typeCode) { + return $proceed($typeCode); + }; + $this->system->cleanAndWarmDefaultScopeData($cleaner); + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml new file mode 100644 index 0000000000000..e672081551d45 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.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="AdminAllowToChooseStateActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'General'. Selects the provided Countries under 'State is Required for'. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="fieldValue" type="string"/> + </arguments> + + <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToAdminConfigGeneralPage"/> + <conditionalClick selector="{{StateOptionsSection.stateOptions}}" dependentSelector="{{StateOptionsSection.countriesWithRequiredRegions}}" visible="false" stepKey="expandStateOptionsTab"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + <scrollTo selector="{{StateOptionsSection.countriesWithRequiredRegions}}" stepKey="scrollToForm"/> + <selectOption selector="{{StateOptionsSection.allowToChooseStateOptionalForCountry}}" userInput="{{fieldValue}}" stepKey="selectStatus"/> + <click selector="#save" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSavingConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml new file mode 100644 index 0000000000000..7b33c5229a869 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminChangeTimeZoneForDifferentWebsiteActionGroup"> + <annotations> + <description>set the time zone for different website</description> + </annotations> + <arguments> + <argument name="websiteName" type="string" defaultValue="{{SimpleProduct.sku}}"/> + <argument name="timeZoneName" type="string"/> + </arguments> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="navigateToLocaleConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad"/> + <click selector="{{LocaleOptionsSection.changeStoreConfigButton}}" stepKey="changeStoreButton"/> + <waitForPageLoad stepKey="waitForStoreOption"/> + <click selector="{{LocaleOptionsSection.changeStoreConfigToSpecificWebsite(websiteName)}}" stepKey="selectNewWebsite"/> + <waitForPageLoad stepKey="waitForWebsiteChange"/> + <!-- Accept the current popup visible on the page. --> + <click selector="{{LocaleOptionsSection.changeWebsiteConfirmButton}}" stepKey="confirmModal"/> + <waitForPageLoad stepKey="waitForSaveChange"/> + <conditionalClick stepKey="expandDefaultLayouts" selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.checkIfTabExpand}}" visible="true"/> + <click selector="{{LocaleOptionsSection.useDefault}}" stepKey="unCheckCheckbox"/> + <waitForElementVisible selector="{{LocaleOptionsSection.timezone}}" stepKey="waitForLocaleTimeZone"/> + <selectOption userInput="{{timeZoneName}}" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectDefaultOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml new file mode 100644 index 0000000000000..0cae2ad05b5b8 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.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="AdminNavigateToDefaultLocaleSettingActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Locale Options'. Expands the 'Locale Options' section.</description> + </annotations> + + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="navigateToLocaleConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick stepKey="expandLocaleOptions" selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.checkIfTabExpand}}" visible="true"/> + <waitForElementVisible selector="{{LocaleOptionsSection.timezone}}" stepKey="waitForLocaleTimeZone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml new file mode 100644 index 0000000000000..055715d71f93c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.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="EuropeanCountriesSystemCheckBoxActionGroup" extends="EuropeanCountriesOptionActionGroup"> + <annotations> + <description>check system value european country option value</description> + </annotations> + + <remove keyForRemoval="uncheckConfigSetting"/> + <checkOption selector="{{CountriesFormSection.useConfigSettings}}" stepKey="checkConfigSetting" after="waitForLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml new file mode 100644 index 0000000000000..99bf20b3e671c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Tax'. Resets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="taxClassForGiftOptions" type="string" defaultValue="None"/> + <argument name="shoppingCartDisplayPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplaySubtotal" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayShippingAmt" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayGiftsWrappingPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayPrintedCardPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayFullTaxSummary" type="string" defaultValue="No"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <selectOption selector="{{SalesConfigSection.TaxClassForGiftOptions}}" userInput="{{taxClassForGiftOptions}}" stepKey="setShippingTaxClassForGiftOptions"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <conditionalClick selector="{{SalesConfigSection.ShoppingCartDisplaySettingsTab}}" dependentSelector="{{SalesConfigSection.ShoppingCartDisplaySettingsTabExpand}}" visible="true" stepKey="expandShoppingCartDisplaySettingsTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="seeDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="uncheckDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="uncheckDisplaySubtotalCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="uncheckDisplayShippingAmountCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="uncheckDisplayGiftsWrappingPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="uncheckDisplayPrintedCardPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="uncheckDisplayFullTaxSummaryCheckbox"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('price')}}" userInput="{{shoppingCartDisplayPrices}}" stepKey="setDisplayPrices"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('subtotal')}}" userInput="{{shoppingCartDisplaySubtotal}}" stepKey="setDisplaySubtotal"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('shipping')}}" userInput="{{shoppingCartDisplayShippingAmt}}" stepKey="setDisplayShipping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('gift_wrapping')}}" userInput="{{shoppingCartDisplayGiftsWrappingPrices}}" stepKey="setDisplayGiftsWrapping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('printed_card')}}" userInput="{{shoppingCartDisplayPrintedCardPrices}}" stepKey="setDisplayPrintedCard"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('full_summary')}}" userInput="{{shoppingCartDisplayFullTaxSummary}}" stepKey="setDisplayFullSummary"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="checkDisplayPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="checkDisplaySubtotalCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="checkDisplayShippingAmountCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="checkDisplayGiftsWrappingPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="checkDisplayPrintedCardPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="checkDisplayFullTaxSummaryCheckbox"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessagePostSavingTheConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml index 3f768bdac8055..b98041d8d6ed5 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml @@ -19,7 +19,7 @@ <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass2"/> <selectOption selector="{{SalesConfigSection.ShippingTaxClass}}" userInput="None" stepKey="resetShippingTaxClass"/> <checkOption selector="{{SalesConfigSection.EnableTaxClassForShipping}}" stepKey="useSystemValue"/> - <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <waitForElementClickable selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml new file mode 100644 index 0000000000000..3a825ebfb511f --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Tax'. Sets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="taxClassForGiftOptions" type="string" defaultValue="Taxable Goods"/> + <argument name="shoppingCartDisplayPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplaySubtotal" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayShippingAmt" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayGiftsWrappingPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayPrintedCardPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayFullTaxSummary" type="string" defaultValue="Yes"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <selectOption selector="{{SalesConfigSection.TaxClassForGiftOptions}}" userInput="{{taxClassForGiftOptions}}" stepKey="setShippingTaxClassForGiftOptions"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <conditionalClick selector="{{SalesConfigSection.ShoppingCartDisplaySettingsTab}}" dependentSelector="{{SalesConfigSection.ShoppingCartDisplaySettingsTabExpand}}" visible="true" stepKey="expandShoppingCartDisplaySettingsTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="seeDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="uncheckDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="uncheckDisplaySubtotalCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="uncheckDisplayShippingAmountCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="uncheckDisplayGiftsWrappingPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="uncheckDisplayPrintedCardPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="uncheckDisplayFullTaxSummaryCheckbox"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('price')}}" userInput="{{shoppingCartDisplayPrices}}" stepKey="setDisplayPrices"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('subtotal')}}" userInput="{{shoppingCartDisplaySubtotal}}" stepKey="setDisplaySubtotal"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('shipping')}}" userInput="{{shoppingCartDisplayShippingAmt}}" stepKey="setDisplayShipping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('gift_wrapping')}}" userInput="{{shoppingCartDisplayGiftsWrappingPrices}}" stepKey="setDisplayGiftsWrapping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('printed_card')}}" userInput="{{shoppingCartDisplayPrintedCardPrices}}" stepKey="setDisplayPrintedCard"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('full_summary')}}" userInput="{{shoppingCartDisplayFullTaxSummary}}" stepKey="setDisplayFullSummary"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessagePostSavingTheConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml index 378aa0bfc510c..59c70ff68f419 100644 --- a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml +++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml @@ -30,4 +30,8 @@ <data key="scope">websites</data> <data key="scope_code">base</data> </entity> + <entity name="SetEuropeanUnionCountries"> + <data key="path">general/country/eu_countries</data> + <data key="value">GB,DE,FR</data> + </entity> </entities> diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml index 99a76a446aaa4..e01d37f6eea2b 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml @@ -11,5 +11,6 @@ <element name="stateOptions" type="button" selector="#general_region-head"/> <element name="countriesWithRequiredRegions" type="select" selector="#general_region_state_required"/> <element name="allowToChooseState" type="select" selector="general_region_display_all"/> + <element name="allowToChooseStateOptionalForCountry" type="select" selector="//td[@class='value']//select[@name='groups[region][fields][display_all][value]']"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml index 878a0c24f7331..f971b4dd03cec 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml @@ -13,5 +13,10 @@ <element name="CheckIfTaxClassesTabExpand" type="button" selector="#tax_classes-head:not(.open)"/> <element name="ShippingTaxClass" type="select" selector="#tax_classes_shipping_tax_class"/> <element name="EnableTaxClassForShipping" type="checkbox" selector="#tax_classes_shipping_tax_class_inherit"/> + <element name="TaxClassForGiftOptions" type="select" selector="#tax_classes_wrapping_tax_class"/> + <element name="ShoppingCartDisplaySettingsTab" type="button" selector="#tax_cart_display-head"/> + <element name="ShoppingCartDisplaySettingsTabExpand" type="button" selector="#tax_cart_display-head:not(.open)"/> + <element name="ParameterizedShoppingCartDisplayCheckbox" type="checkbox" selector="//input[@name='groups[cart_display][fields][{{arg}}][inherit]']" parameterized="true"/> + <element name="ParameterizedShoppingCartDisplayDropdown" type="select" selector="//input[@name='groups[cart_display][fields][{{arg}}][inherit]']/../..//select" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml new file mode 100644 index 0000000000000..cd25471a0de4c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DateFiltersInCustomInstanceTimeZoneTest"> + <annotations> + <features value="Config"/> + <stories value="Verify that Date filters of new Data Grids in Admin provide relevant search results if custom Instance Timezone is set"/> + <title value="Verify DateFilters"/> + <description value="Verify that Date filters of new Data Grids in Admin provide relevant search results if custom Instance Timezone is set"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4300"/> + </annotations> + <before> + <!--Login To Admin panel--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Go to *General > General > Locale Options* section --> + <actionGroup ref="AdminNavigateToDefaultLocaleSettingActionGroup" stepKey="redirect"/> + <!--Set needed Timezone--> + <selectOption userInput="New Zealand Standard Time (Antarctica/McMurdo)" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectOption1"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminNavigateToDefaultLocaleSettingActionGroup" stepKey="redirectAgain"/> + <selectOption userInput="Central Standard Time (America/Chicago)" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectDefaultoption"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <!-- Delete customer --> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </after> + <!-- Create Customer --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="Simple_US_Customer"/> + </actionGroup> + <!--Login to Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <!--Go to *Customers > All Customers* page--> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> + </actionGroup> + <!--Clear Filters if Present on Customer Grid Page--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <!-- Click on Filters--> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <!-- Generate Today's Date to set in filter--> + <!--<generateDate date="now" format="m/d/Y" stepKey="today"/>--> + <generateDate date="now" format="m/j/Y" timezone="Antarctica/McMurdo" stepKey="today"/> + <!--Set the *Customer Since* filter From Date--> + <fillField selector="{{AdminDataGridHeaderSection.dateFilterFrom}}" userInput="{$today}" stepKey="fillDateFrom"/> + <!--Set the *Customer Since* filter To Date--> + <fillField selector="{{AdminDataGridHeaderSection.dateFilterTo}}" userInput="{$today}" stepKey="fillDateto"/> + <!-- Apply Filter--> + <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applyFilter"/> + <!--Customer *A* is present in the grid--> + <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="assertCustomer1InGrid"> + <argument name="text" value="{{Simple_US_Customer.email}}"/> + <argument name="row" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml index 72a23f7e811c0..c1131d2d701e2 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="config"/> <testCaseId value="AC-6385"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php index 0a322457ed741..1a4fa9915cc4e 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php @@ -9,7 +9,7 @@ use Magento\Config\Model\Config\Loader; use Magento\Config\Model\ResourceModel\Config\Data\Collection; -use Magento\Framework\App\Config\Value; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; use Magento\Framework\App\Config\ValueFactory; use Magento\Framework\DataObject; use PHPUnit\Framework\MockObject\MockObject; @@ -23,15 +23,20 @@ class LoaderTest extends TestCase protected $_model; /** - * @var MockObject + * @var MockObject&ValueFactory */ protected $_configValueFactory; /** - * @var MockObject + * @var MockObject&Collection */ protected $_configCollection; + /** + * @var MockObject&CollectionFactory + */ + protected $collectionFactory; + protected function setUp(): void { $this->_configValueFactory = $this->getMockBuilder(ValueFactory::class) @@ -39,41 +44,19 @@ protected function setUp(): void ->onlyMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->_model = new Loader($this->_configValueFactory); + $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->addMethods(['getCollection']) + ->onlyMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->_model = new Loader($this->_configValueFactory, $this->collectionFactory); $this->_configCollection = $this->createMock(Collection::class); - $this->_configCollection->expects( - $this->once() - )->method( - 'addScopeFilter' - )->with( - 'scope', - 'scopeId', - 'section' - )->willReturnSelf(); - - $configDataMock = $this->createMock(Value::class); - $this->_configValueFactory->expects( - $this->once() - )->method( - 'create' - )->willReturn( - $configDataMock - ); - $configDataMock->expects( - $this->any() - )->method( - 'getCollection' - )->willReturn( - $this->_configCollection - ); - - $this->_configCollection->expects( - $this->once() - )->method( - 'getItems' - )->willReturn( - [new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])] - ); + $this->_configCollection->expects($this->once())-> + method('addScopeFilter')->with('scope', 'scopeId', 'section')->willReturnSelf(); + $this->_configValueFactory->expects($this->never())->method('create'); + $this->collectionFactory->expects($this->any())->method('create')->willReturn($this->_configCollection); + $this->_configCollection->expects($this->once())->method('getItems') + ->willReturn([new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])]); } protected function tearDown(): void diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index deb2c4ed4a483..478e75e3f06e5 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -175,6 +175,8 @@ protected function setUp(): void */ public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void { + $this->appConfigMock->expects($this->never()) + ->method('reinit'); $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); $this->model->save(); } @@ -184,6 +186,8 @@ public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void */ public function testSaveEmptiesNonSetArguments(): void { + $this->appConfigMock->expects($this->never()) + ->method('reinit'); $this->structureReaderMock->expects($this->never())->method('getConfiguration'); $this->assertNull($this->model->getSection()); $this->assertNull($this->model->getWebsite()); @@ -199,6 +203,8 @@ public function testSaveEmptiesNonSetArguments(): void */ public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); @@ -227,6 +233,8 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void */ public function testDoNotSaveReadOnlyFields(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); @@ -265,6 +273,8 @@ public function testDoNotSaveReadOnlyFields(): void */ public function testSaveToCheckScopeDataSet(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 4536bc71c6c10..5052e7d0fba87 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -380,4 +380,7 @@ <argument name="configStructure" xsi:type="object">\Magento\Config\Model\Config\Structure\Proxy</argument> </arguments> </type> + <type name="Magento\Framework\App\Cache\TypeList"> + <plugin name="warm_config_cache" type="Magento\Config\Plugin\Framework\App\Cache\TypeList\WarmConfigCache"/> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php index 75d8b3635d0ee..8be73279478c4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php @@ -11,11 +11,12 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Extender of product identities for child of configurable products */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var ConfigurableType @@ -79,4 +80,12 @@ private function getParentIdsByChild($childId) return $this->cacheParentIdsByChild[$childId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheParentIdsByChild = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 7f228caeb3e46..c2f95bebdb887 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -18,6 +18,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\File\UploaderFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Configurable product type implementation @@ -31,7 +32,7 @@ * @api * @since 100.0.2 */ -class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType +class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType implements ResetAfterRequestInterface { /** * Product type code @@ -1494,4 +1495,13 @@ function($attr) { return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes)); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isSaleableBySku = []; + } + } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index df2a9707f18d5..50421c6967b36 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -9,14 +9,14 @@ use Magento\Catalog\Model\Product\Type as ProductType; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** - * Variation Handler * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 */ -class VariationHandler +class VariationHandler implements ResetAfterRequestInterface { /** * @var \Magento\Catalog\Model\Product\Gallery\Processor @@ -52,6 +52,7 @@ class VariationHandler /** * @var \Magento\CatalogInventory\Api\StockConfigurationInterface * @deprecated 100.1.0 + * @see MSI */ protected $stockConfiguration; @@ -120,6 +121,7 @@ public function generateSimpleProducts($parentProduct, $productsData) * Prepare attribute set comprising all selected configurable attributes * * @deprecated 100.1.0 + * @see prepareAttributeSet() * @param \Magento\Catalog\Model\Product $product * @return void */ @@ -198,7 +200,10 @@ protected function fillSimpleProductData( continue; } - $product->setData($attribute->getAttributeCode(), $parentProduct->getData($attribute->getAttributeCode())); + $product->setData( + $attribute->getAttributeCode(), + $parentProduct->getData($attribute->getAttributeCode()) ?? $attribute->getDefaultValue() + ); } $keysFilter = ['item_id', 'product_id', 'stock_id', 'type_id', 'website_id']; @@ -298,4 +303,12 @@ function ($image) { } return $productData; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php index 6434cf65bfd67..c2da11493a0ee 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php @@ -9,11 +9,12 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Provide configurable child products for price calculation */ -class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface +class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface, ResetAfterRequestInterface { /** * @var Configurable @@ -56,4 +57,12 @@ public function getProducts(ProductInterface $product) } return $this->products[$product->getId()]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->products = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php index a6a6b8753824f..25f1a464e3b5c 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php @@ -8,17 +8,20 @@ use Magento\Catalog\Model\Product; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Price\AbstractPrice; /** * Class RegularPrice */ -class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegularPriceInterface +class ConfigurableRegularPrice extends AbstractPrice implements + ConfigurableRegularPriceInterface, + ResetAfterRequestInterface { /** * Price type */ - const PRICE_CODE = 'regular_price'; + public const PRICE_CODE = 'regular_price'; /** * @var \Magento\Framework\Pricing\Amount\AmountInterface @@ -73,7 +76,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getValue() { @@ -85,7 +88,7 @@ public function getValue() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAmount() { @@ -93,7 +96,7 @@ public function getAmount() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxRegularAmount() { @@ -121,7 +124,7 @@ protected function doGetMaxRegularAmount() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMinRegularAmount() { @@ -159,8 +162,11 @@ protected function getUsedProducts() } /** + * Retrieve Configurable Option Provider + * * @return \Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface * @deprecated 100.1.1 + * @see we don't recommend this approach anymore */ private function getConfigurableOptionsProvider() { @@ -170,4 +176,12 @@ private function getConfigurableOptionsProvider() } return $this->configurableOptionsProvider; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->values = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/README.md b/app/code/Magento/ConfigurableProduct/README.md index 1a693b0db94eb..d495fca96f40d 100644 --- a/app/code/Magento/ConfigurableProduct/README.md +++ b/app/code/Magento/ConfigurableProduct/README.md @@ -35,7 +35,7 @@ Value | Description If the `gallery_switch_strategy` variable is not defined, the default value `replace` will be used. -For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode. +For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode. ```xml <vars module="Magento_ConfigurableProduct"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml new file mode 100644 index 0000000000000..cc0b31a5b3c9c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.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="AddNewProductConfigurationWithThreeAttributeActionGroup" extends="AddNewProductConfigurationAttributeActionGroup"> + <annotations> + <description>Generates the Product Configurations for the 3 provided Attribute Names on the Configurable Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="thirdOption" type="entity"/> + </arguments> + + <!-- Find created below attribute and add option; save attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" after="clickOnSaveAttribute" stepKey="clickOnCreateThirdNewValue"/> + <fillField userInput="{{thirdOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" after="clickOnCreateThirdNewValue" stepKey="fillFieldForNewThirdOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" after="fillFieldForNewThirdOption" stepKey="clickOnSaveThirdAttribute"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml new file mode 100644 index 0000000000000..1f9eb4d4bd3aa --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChangeProductConfigurationsWithThirdInGridActionGroup" extends="ChangeProductConfigurationsInGridActionGroup"> + <annotations> + <description>Edit the Product Configuration with 3rd attribute via the Admin Product grid page.</description> + </annotations> + <arguments> + <argument name="thirdOption" type="entity"/> + </arguments> + + <fillField userInput="{{thirdOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(thirdOption.name)}}" after="fillFieldNameForSecondAttributeOption" stepKey="fillFieldNameForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(thirdOption.name)}}" after="fillFieldNameForThirdAttributeOption" stepKey="fillFieldSkuForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(thirdOption.name)}}" after="fillFieldSkuForThirdAttributeOption" stepKey="fillFieldPriceForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(thirdOption.name)}}" after="fillFieldPriceForThirdAttributeOption" stepKey="fillFieldQuantityForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(thirdOption.name)}}" after="fillFieldQuantityForThirdAttributeOption" stepKey="fillFieldWeightForThirdAttributeOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml index d2873e79a8b89..de0225d509d6a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -65,14 +65,14 @@ <data key="quantity">10</data> <data key="weight">1</data> </entity> - + <entity name="colorConfigurableProductAttribute3" type="product_attribute"> <data key="name" unique="suffix">Black</data> <data key="sku" unique="suffix">sku-black</data> <data key="type_id">simple</data> <data key="price">2</data> <data key="visibility">1</data> - <data key="quantity">10</data> + <data key="quantity">6</data> <data key="weight">1</data> </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml index 22cb822dbe762..1ceb33a231c99 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml @@ -53,5 +53,12 @@ <element name="fileUploaderInput" type="file" selector="//input[@type='file' and @class='file-uploader-input']"/> <element name="variationImageSource" type="text" selector="[data-index='configurable-matrix'] [data-index='thumbnail_image_container'] img[src*='{{imageName}}']" parameterized="true"/> <element name="variationProductLinkByName" type="text" selector="//div[@data-index='configurable-matrix']//*[@data-index='name_container']//a[contains(text(), '{{productName}}')]" parameterized="true"/> + <element name="unAssignSource" type="button" selector="//span[text()='{{source_name}}']/../../..//button[@class='action-delete']//span[text()='Unassign']" parameterized="true"/> + <element name="btnAssignSources" type="button" selector="//button//span[text()='Assign Sources']/.."/> + <element name="chkSourceToAssign" type="checkbox" selector="//input[@id='idscheck{{source_id}}']/.." parameterized="true"/> + <element name="btnDoneAssignedSources" type="button" selector="//aside[@class='modal-slide product_form_product_form_sources_assign_sources_modal _show']//button[@class='action-primary']//span[text()='Done']/.." /> + <element name="searchBySource" type="input" selector="//div[contains(@data-bind,'inventory_source_listing.inventory_source_listing')]/div[2]//input[@placeholder='Search by keyword']"/> + <element name="clickSearch" type="button" selector="//div[contains(@data-bind,'inventory_source_listing.inventory_source_listing')]/div[2]//button[@aria-label='Search']"/> + <element name="btnDoneAdvancedInventory" type="button" selector="//aside[@class='modal-slide product_form_product_form_advanced_inventory_modal _show']//button[@class='action-primary']//span[text()='Done']/.." /> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml index 07ed24d9bdbc7..521481c992a9a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-101"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml index 80adfebb57f7a..747002b55029c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml @@ -15,6 +15,7 @@ <description value="admin should be able to create a configurable product with tier prices"/> <severity value="MAJOR"/> <testCaseId value="AC-4468"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index e82efcf811359..71933dc32d2b1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17450"/> <useCaseId value="MAGETWO-99443"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml index f4cad6590e1f6..bc56c333ac4fb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-6192"/> <useCaseId value="MAGETWO-91753"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- Create default category with subcategory --> @@ -200,5 +201,6 @@ <waitForPageLoad stepKey="waitForNewSimpleProductPage"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageThird"/> <magentoCron stepKey="runCronIndex" groups="index"/> + <waitForPageLoad stepKey="waitForPageLoadAfterReindex"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml index 7dfd0bffaa3c6..84f4f5c53bcca 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml @@ -17,6 +17,7 @@ <useCaseId value="ACP2E-101"/> <severity value="MAJOR"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml index 9e558659229cb..6048972adad3e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-84"/> <group value="ConfigurableProduct"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml index 60bc6182b09b7..7c1e86105492a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-96365"/> <useCaseId value="MAGETWO-94556"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml index 33a77a96a6bcd..6a8ff4bdfe6da 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-99"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml index b2fe25e9691a3..7cf6691c2e4f4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-87"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml index 6093e39e899c6..4edc7e2e09504 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4289"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- create category --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index 68d60dfa90e65..d8e5fea3cdc85 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-5685"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml index 893cfd3fa5338..dc618a6b64595 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3042"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml index a741272bfca0c..9458226c2a552 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-95"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml index f7e171c23f8d7..24f71a3253abe 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-119"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml index 08ab165f95682..3d879405bb4a4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-63"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml index ae6de82987a99..f342e3d0c5e0d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml @@ -18,6 +18,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="configurable"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml index 17c7426dc547f..f19fc7556b30d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-29398"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml index 75c699d7299a8..8e56475575f4a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13689"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 36d1eb799c19f..afedf6ee5b5c2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13713"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create first attribute with 2 options --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml index 990d7a7dfbc41..f2c709a6b09d5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13714"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create attribute with 3 options to be used in children products --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml index 1f39a49fb277e..d46613b99caea 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11020"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml index bf92d6c886937..40dbdf9c65e98 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!--Delete product configurations--> <comment userInput="Delete product configuration" stepKey="commentDeleteConfigs"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml index 63e38c5aa2c06..7dbf05cb8cfec 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml index e26759892a07e..c7367ac58fef1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml index 076d55025aca5..4bd2af5240f0c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-196"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml index c8bc4541015d7..76e86ac218f17 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml @@ -15,6 +15,7 @@ <description value="Configurable Product with an Out of Stock Item in Shopping Cart"/> <testCaseId value="AC-4310"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml index c620023ae102f..ba685f577991f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml @@ -15,6 +15,7 @@ <description value="Customer Reorder Configurable Product"/> <testCaseId value="MC-26757"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- create category --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml index 06942f69672e4..b71daa0aac9bc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml @@ -16,6 +16,7 @@ <description value="Already selected configurable option should be selected when configurable product is edited from minicart"/> <severity value="MAJOR"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml index b75dd590dbbf1..1cefe06bff514 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -129,7 +129,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProductForm"/> <!-- Go to created customer page --> <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> @@ -145,7 +145,7 @@ <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" stepKey="dontSeeOption1"/> <!-- Go to created customer page again --> <comment userInput="Go to created customer page again" stepKey="goToCreatedCustomerPageAgain"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderAgain"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderAgain"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProductAgain"/> @@ -160,7 +160,7 @@ <waitForPageLoad stepKey="waitForNewOrderPageLoad"/> <see userInput="There are no source items with the in stock status" stepKey="seeTheErrorMessageDisplayed"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderThirdTime"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderThirdTime"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addThirdChildProductToOrder"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml index 9fe38d3fd6117..71493e47d6e27 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-77"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml index 0348570f63909..965165eeae4f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-97"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml index c4dcccc53c8d8..17e03496d2b47 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-81"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml index a2d1b2c077f2d..62d94edc10893 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-92"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml index 678c6f99a9f2a..66fd0544be274 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml @@ -19,6 +19,7 @@ <group value="catalog"/> <group value="configurableProduct"/> <group value="swatch"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml index b23f59ffbc861..4de10a9c3264e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-89"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml index 8f7924c3f3094..b36986813298b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-61"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml index ea4a0607d1d4b..0e3466957277c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml @@ -19,6 +19,7 @@ <group value="catalog"/> <group value="configurableProduct"/> <group value="swatch"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index ea309271abace..1f90819b5cb15 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="ConfigurableProduct"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 5f68a14a36193..0c6365ad5aa17 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17226"/> <useCaseId value="MAGETWO-64923"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js index f5c9382af0bc3..240fa180fd871 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js @@ -135,20 +135,8 @@ define([ if (productId && !images.file) { images = product.images; } - productDataFromGrid = _.pick( - productDataFromGrid, - 'sku', - 'name', - 'weight', - 'status', - 'price', - 'qty' - ); + productDataFromGrid = this.prepareProductDataFromGrid(productDataFromGrid); - if (productDataFromGrid.hasOwnProperty('qty')) { - productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty; - } - delete productDataFromGrid.qty; product = _.pick( product || {}, 'sku', @@ -288,6 +276,32 @@ define([ * Back. */ back: function () { + }, + + /** + * Prepare product data from grid to have all the current fields values + * + * @param {Object} productDataFromGrid + * @return {Object} + */ + prepareProductDataFromGrid: function (productDataFromGrid) { + productDataFromGrid = _.pick( + productDataFromGrid, + 'sku', + 'name', + 'weight', + 'status', + 'price', + 'qty' + ); + + if (productDataFromGrid.hasOwnProperty('qty')) { + productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty; + } + + delete productDataFromGrid.qty; + + return productDataFromGrid; } }); }); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index cbe840c95795f..9d19500bf6054 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -279,7 +279,7 @@ define([ _configureElement: function (element) { this.simpleProduct = this._getSimpleProductId(element); - if (element.value) { + if (element.value && element.config) { this.options.state[element.config.id] = element.value; if (element.nextSetting) { @@ -298,9 +298,11 @@ define([ } this._reloadPrice(); - this._displayRegularPriceBlock(this.simpleProduct); - this._displayTierPriceBlock(this.simpleProduct); - this._displayNormalPriceLabel(); + if (element.config) { + this._displayRegularPriceBlock(this.simpleProduct); + this._displayTierPriceBlock(this.simpleProduct); + this._displayNormalPriceLabel(); + } this._changeProductImage(); }, @@ -372,7 +374,7 @@ define([ */ _sortImages: function (images) { return _.sortBy(images, function (image) { - return image.position; + return parseInt(image.position, 10); }); }, @@ -439,8 +441,10 @@ define([ filteredSalableProducts; this._clearSelect(element); - element.options[0] = new Option('', ''); - element.options[0].innerHTML = this.options.spConfig.chooseText; + if (element.options) { + element.options[0] = new Option('', ''); + element.options[0].innerHTML = this.options.spConfig.chooseText; + } prevConfig = false; if (element.prevSetting) { @@ -552,8 +556,10 @@ define([ _clearSelect: function (element) { var i; - for (i = element.options.length - 1; i >= 0; i--) { - element.remove(i); + if (element.options) { + for (i = element.options.length - 1; i >= 0; i--) { + element.remove(i); + } } }, @@ -585,26 +591,31 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - allowedProduct; + allowedProduct, + selected, + config, + priceValue; _.each(elements, function (element) { - var selected = element.options[element.selectedIndex], - config = selected && selected.config, + if (element.options) { + selected = element.options[element.selectedIndex]; + config = selected && selected.config; priceValue = this._calculatePrice({}); - if (config && config.allowedProducts.length === 1) { - priceValue = this._calculatePrice(config); - } else if (element.value) { - allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); - priceValue = this._calculatePrice({ - 'allowedProducts': [ - allowedProduct - ] - }); - } + if (config && config.allowedProducts.length === 1) { + priceValue = this._calculatePrice(config); + } else if (element.value) { + allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); + } - if (!_.isEmpty(priceValue)) { - prices.prices = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } } }, this); @@ -664,19 +675,23 @@ define([ _getSimpleProductId: function (element) { // TODO: Rewrite algorithm. It should return ID of // simple product based on selected options. - var allOptions = element.config.options, - value = element.value, + var allOptions, + value, config; - config = _.filter(allOptions, function (option) { - return option.id === value; - }); - config = _.first(config); + if (element.config) { + allOptions = element.config.options; + value = element.value; - return _.isEmpty(config) ? - undefined : - _.first(config.allowedProducts); + config = _.filter(allOptions, function (option) { + return option.id === value; + }); + config = _.first(config); + return _.isEmpty(config) ? + undefined : + _.first(config.allowedProducts); + } }, /** diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index fa8b669a1bdd3..8e57ae7cf5ba7 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -17,11 +17,12 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Collection for fetching options for all configurable options pulled back in result set. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * Option type name @@ -159,4 +160,13 @@ function ($value) use ($attribute) { return $this->attributeMap; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productIds = []; + $this->attributeMap = []; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php index c42a020a2fb6f..b9158cc89176f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -7,11 +7,11 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; -use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterfaceFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\Amount\BaseFactory; use Magento\Framework\Pricing\SaleableInterface; @@ -19,13 +19,18 @@ /** * Provides product prices for configurable products */ -class Provider implements ProviderInterface +class Provider implements ProviderInterface, ResetAfterRequestInterface { /** * @var ConfigurableOptionsProviderInterface */ private $optionsProvider; + /** + * @var ConfigurableOptionsProviderInterfaceFactory + */ + private $optionsProviderFactory; + /** * @var BaseFactory */ @@ -48,14 +53,15 @@ class Provider implements ProviderInterface ]; /** - * @param ConfigurableOptionsProviderInterface $optionsProvider + * @param ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory * @param BaseFactory $amountFactory */ public function __construct( - ConfigurableOptionsProviderInterface $optionsProvider, + ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory, BaseFactory $amountFactory ) { - $this->optionsProvider = $optionsProvider; + $this->optionsProvider = $optionsProviderFactory->create(); + $this->optionsProviderFactory = $optionsProviderFactory; $this->amountFactory = $amountFactory; } @@ -144,4 +150,16 @@ private function getMaximalPrice(SaleableInterface $product, string $code): Amou return $this->maximalPrice[$code][$product->getId()] ?? $this->amountFactory->create(['amount' => null]); } + + /** + * @inheritDoc + */ + public function _resetState():void + { + $this->minimalPrice[RegularPrice::PRICE_CODE] = []; + $this->minimalPrice[FinalPrice::PRICE_CODE] = []; + $this->maximalPrice[RegularPrice::PRICE_CODE] = []; + $this->maximalPrice[FinalPrice::PRICE_CODE] = []; + $this->optionsProvider = $this->optionsProviderFactory->create(); + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index bc67046dee8d3..fac7e82ce4666 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -14,6 +14,7 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; @@ -22,7 +23,7 @@ /** * Collection for fetching configurable child product data. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -201,4 +202,14 @@ private function getAttributesCodes(Product $currentProduct): array return $attributeCodes; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->parentProducts = []; + $this->childrenMap = []; + $this->attributeCodes = []; + } } diff --git a/app/code/Magento/ConfigurableProductSales/README.md b/app/code/Magento/ConfigurableProductSales/README.md index af915a8265827..f49c6c0284d34 100644 --- a/app/code/Magento/ConfigurableProductSales/README.md +++ b/app/code/Magento/ConfigurableProductSales/README.md @@ -1,4 +1,4 @@ # Magento_ConfigurableProductSales module The Magento_ConfigurableProductSales module checks that the selected options of order item are still presented in -Catalog. Returns true if the previously ordered item configuration is still available. \ No newline at end of file +Catalog. Returns true if the previously ordered item configuration is still available. diff --git a/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php new file mode 100644 index 0000000000000..e608df9db8b2d --- /dev/null +++ b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ContactGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress; + +class ContactUsValidator +{ + /** + * @var EmailAddress + */ + private EmailAddress $emailValidator; + + /** + * @param EmailAddress $emailValidator + */ + public function __construct( + EmailAddress $emailValidator + ) { + $this->emailValidator = $emailValidator; + } + + /** + * Validate input data + * + * @param string[] $input + * @return void + * @throws GraphQlInputException + */ + public function execute(array $input): void + { + if (!$this->emailValidator->isValid($input['email'])) { + throw new GraphQlInputException( + __('The email address is invalid. Verify the email address and try again.') + ); + } + + if ($input['name'] === '') { + throw new GraphQlInputException(__('Name field is required.')); + } + + if ($input['comment'] === '') { + throw new GraphQlInputException(__('Comment field is required.')); + } + } +} diff --git a/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php new file mode 100644 index 0000000000000..eb6e852358579 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php @@ -0,0 +1,95 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ContactGraphQl\Model\Resolver; + +use Magento\Contact\Model\ConfigInterface; +use Magento\Contact\Model\MailInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Psr\Log\LoggerInterface; +use Magento\ContactGraphQl\Model\ContactUsValidator; + +class ContactUs implements ResolverInterface +{ + /** + * @var MailInterface + */ + private MailInterface $mail; + + /** + * @var ConfigInterface + */ + private ConfigInterface $contactConfig; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @var ContactUsValidator + */ + private ContactUsValidator $validator; + + /** + * @param MailInterface $mail + * @param ConfigInterface $contactConfig + * @param LoggerInterface $logger + * @param ContactUsValidator $validator + */ + public function __construct( + MailInterface $mail, + ConfigInterface $contactConfig, + LoggerInterface $logger, + ContactUsValidator $validator + ) { + $this->mail = $mail; + $this->contactConfig = $contactConfig; + $this->logger = $logger; + $this->validator = $validator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->contactConfig->isEnabled()) { + throw new GraphQlInputException( + __('The contact form is unavailable.') + ); + } + + $input = array_map(function ($field) { + return $field === null ? '' : trim($field); + }, $args['input']); + $this->validator->execute($input); + + try { + $this->mail->send($input['email'], ['data' => $input]); + } catch (\Exception $e) { + $this->logger->critical($e); + throw new GraphQlInputException( + __('An error occurred while processing your form. Please try again later.') + ); + } + + return [ + 'status' => true + ]; + } +} diff --git a/app/code/Magento/ContactGraphQl/README.md b/app/code/Magento/ContactGraphQl/README.md new file mode 100644 index 0000000000000..0d983ddf4a4e4 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/README.md @@ -0,0 +1,3 @@ +# ContactGraphQlPwa + +**ContactGraphQlPwa** provides GraphQL support for `magento/module-contact`. diff --git a/app/code/Magento/Elasticsearch8/composer.json b/app/code/Magento/ContactGraphQl/composer.json similarity index 50% rename from app/code/Magento/Elasticsearch8/composer.json rename to app/code/Magento/ContactGraphQl/composer.json index 11a6d78f33f4a..9c08ecbb16758 100644 --- a/app/code/Magento/Elasticsearch8/composer.json +++ b/app/code/Magento/ContactGraphQl/composer.json @@ -1,19 +1,18 @@ { - "name": "magento/module-elasticsearch-8", + "name": "magento/module-contact-graph-ql", "description": "N/A", + "type": "magento2-module", + "config": { + "sort-packages": true + }, "require": { "php": "~8.1.0||~8.2.0", "magento/framework": "*", - "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "^8.5", - "magento/module-advanced-search": "*", - "magento/module-catalog-search": "*", - "magento/module-search": "*" + "magento/module-contact": "*" }, "suggest": { - "magento/module-config": "*" + "magento/module-graph-ql": "*" }, - "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" @@ -23,7 +22,7 @@ "registration.php" ], "psr-4": { - "Magento\\Elasticsearch8\\": "" + "Magento\\ContactGraphQl\\": "" } } } diff --git a/app/code/Magento/ContactGraphQl/etc/graphql/di.xml b/app/code/Magento/ContactGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..b46225b07eede --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/graphql/di.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:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="contact_enabled" xsi:type="string">contact/contact/enabled</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/ContactGraphQl/etc/module.xml b/app/code/Magento/ContactGraphQl/etc/module.xml new file mode 100644 index 0000000000000..801683ba43618 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_ContactGraphQl" /> +</config> diff --git a/app/code/Magento/ContactGraphQl/etc/schema.graphqls b/app/code/Magento/ContactGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..400c5471d942f --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/schema.graphqls @@ -0,0 +1,23 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + contactUs( + input: ContactUsInput! @doc(description: "An input object that defines shopper information.") + ): ContactUsOutput @doc(description: "Send a 'Contact Us' email to the merchant.") @resolver(class: "Magento\\ContactGraphQl\\Model\\Resolver\\ContactUs") +} + +input ContactUsInput { + email: String! @doc(description: "The email address of the shopper.") + name: String! @doc(description: "The full name of the shopper.") + telephone: String @doc(description: "The shopper's telephone number.") + comment: String! @doc(description: "The shopper's comment to the merchant.") +} + +type ContactUsOutput @doc(description: "Contains the status of the request."){ + status: Boolean! @doc(description: "Indicates whether the request was successful.") +} + +type StoreConfig { + contact_enabled: Boolean! @doc(description: "Indicates whether the Contact Us form in enabled.") +} diff --git a/app/code/Magento/ContactGraphQl/registration.php b/app/code/Magento/ContactGraphQl/registration.php new file mode 100644 index 0000000000000..27782c62d7966 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_ContactGraphQl', __DIR__); diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml index 160b5448d5132..a0117f76414eb 100644 --- a/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml +++ b/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Cookie"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Cron/README.md b/app/code/Magento/Cron/README.md index 445666301ade4..47238153f9cad 100644 --- a/app/code/Magento/Cron/README.md +++ b/app/code/Magento/Cron/README.md @@ -1,2 +1,2 @@ Cron is a module that enables scheduling of jobs. Other modules can add cron jobs by including crontab.xml in their etc directory. The command "bin/magento cron:run" should be run periodically to trigger the Cron module to run its scheduled jobs. -This module also allows administrators to tune cron options in Magento Admin. \ No newline at end of file +This module also allows administrators to tune cron options in Magento Admin. diff --git a/app/code/Magento/Csp/README.md b/app/code/Magento/Csp/README.md index 0cd2cbb907054..6006f5cf14500 100644 --- a/app/code/Magento/Csp/README.md +++ b/app/code/Magento/Csp/README.md @@ -1,4 +1,5 @@ # Magento_Csp module + Magento_Csp implements Content Security Policies for Magento. Allows CSP configuration for Merchants, provides a way for extension and theme developers to configure CSP headers for their extensions. diff --git a/app/code/Magento/CurrencySymbol/README.md b/app/code/Magento/CurrencySymbol/README.md index 39fb926e410de..c839781f5594a 100644 --- a/app/code/Magento/CurrencySymbol/README.md +++ b/app/code/Magento/CurrencySymbol/README.md @@ -5,11 +5,12 @@ ## Controllers ### Currency Controllers + ***CurrencySymbol\Controller\Adminhtml\System\Currency\FetchRates.php*** gets a specified currency conversion rate. Supports all defined currencies in the system. ***CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates.php*** saves rates for defined currencies. ### Currency Symbol Controllers + ***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Reset.php*** resets all custom currency symbols. ***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Save.php*** creates custom currency symbols. - diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml index 77d00b09d655c..2611c4903c47f 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml @@ -13,8 +13,8 @@ <argument name="currency" type="string" defaultValue="EUR"/> </arguments> <click selector="{{StorefrontSwitchCurrencyRatesSection.currencyToggle}}" stepKey="openToggle"/> - <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="waitForCurrency"/> - <click selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="chooseCurrency"/> + <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currencySwitcherDropdown}}" stepKey="waitForCurrency"/> + <conditionalClick selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" dependentSelector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" visible="true" stepKey="chooseCurrency"/> <waitForPageLoad stepKey="waitForPageLoad"/> <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" stepKey="waitForSelectedCurrency"/> <see selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" userInput="{{currency}}" stepKey="seeSelectedCurrency"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml index 43512796a134d..8dc00a759c2fe 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml @@ -12,5 +12,6 @@ <element name="currencyToggle" 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"/> + <element name="currencySwitcherDropdown" type="block" selector="#switcher-currency ul.switcher-dropdown" /> </section> </sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml index 91ae96e2656df..e6086c427a708 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml @@ -18,6 +18,8 @@ <testCaseId value="MC-28786"/> <useCaseId value="MAGETWO-94919"/> <group value="currency"/> + <!-- Remove this group when Subscription is finalized or Mocking is enabled --> + <group value="pr_exclude" /> </annotations> <before> <!--Set currency configuration--> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml index 7c1b918a0528a..6600c46d836d7 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml index 77b2fc9f32330..a7b6b9d36f9ad 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index 9c607be9f217c..165233cc6a880 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Api; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; /** * Interface for managing customers accounts. @@ -194,7 +195,7 @@ public function resendConfirmation($email, $websiteId, $redirectUrl = ''); * Check if given email is associated with a customer account in given website. * * @param string $customerEmail - * @param int $websiteId If not set, will use the current websiteId + * @param int|null $websiteId If not set, will use the current websiteId * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php index 0d94a01698b31..698ed0d03390b 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php @@ -46,8 +46,6 @@ class Newsletter extends Generic implements TabInterface protected $customerAccountManagement; /** - * Core registry - * * @var Registry */ protected $_coreRegistry = null; @@ -414,7 +412,7 @@ protected function updateFromSession(Form $form, $customerId) */ public function getStatusChangedDate() { - $customer = $this->getCurrentCustomerId(); + $customer = $this->getCurrentCustomer(); if ($customer === null) { return ''; } diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index 71c2a86a98a6a..d215a935545eb 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -1,9 +1,10 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Controller\Account; use Magento\Customer\Api\AccountManagementInterface; @@ -15,11 +16,15 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Exception\StateException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Phrase; use Magento\Framework\UrlFactory; +use Magento\Framework\Exception\StateException; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Logger as CustomerLogger; /** * Class Confirm @@ -65,6 +70,21 @@ class Confirm extends AbstractAccount implements HttpGetActionInterface */ protected $session; + /** + * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory + */ + private $cookieMetadataFactory; + + /** + * @var \Magento\Framework\Stdlib\Cookie\PhpCookieManager + */ + private $cookieMetadataManager; + + /** + * @var CustomerLogger + */ + private CustomerLogger $customerLogger; + /** * @param Context $context * @param Session $customerSession @@ -74,6 +94,7 @@ class Confirm extends AbstractAccount implements HttpGetActionInterface * @param CustomerRepositoryInterface $customerRepository * @param Address $addressHelper * @param UrlFactory $urlFactory + * @param CustomerLogger|null $customerLogger */ public function __construct( Context $context, @@ -83,7 +104,8 @@ public function __construct( AccountManagementInterface $customerAccountManagement, CustomerRepositoryInterface $customerRepository, Address $addressHelper, - UrlFactory $urlFactory + UrlFactory $urlFactory, + ?CustomerLogger $customerLogger = null ) { $this->session = $customerSession; $this->scopeConfig = $scopeConfig; @@ -92,9 +114,40 @@ public function __construct( $this->customerRepository = $customerRepository; $this->addressHelper = $addressHelper; $this->urlModel = $urlFactory->create(); + $this->customerLogger = $customerLogger ?? ObjectManager::getInstance()->get(CustomerLogger::class); parent::__construct($context); } + /** + * Retrieve cookie manager + * + * @return \Magento\Framework\Stdlib\Cookie\PhpCookieManager + */ + private function getCookieManager() + { + if (!$this->cookieMetadataManager) { + $this->cookieMetadataManager = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class + ); + } + return $this->cookieMetadataManager; + } + + /** + * Retrieve cookie metadata factory + * + * @return \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory + */ + private function getCookieMetadataFactory() + { + if (!$this->cookieMetadataFactory) { + $this->cookieMetadataFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class + ); + } + return $this->cookieMetadataFactory; + } + /** * Confirm customer account by id and confirmation key * @@ -110,7 +163,7 @@ public function execute() return $resultRedirect; } - $customerId = $this->getRequest()->getParam('id', false); + $customerId = $this->getCustomerId(); $key = $this->getRequest()->getParam('key', false); if (empty($customerId) || empty($key)) { $this->messageManager->addErrorMessage(__('Bad request.')); @@ -119,10 +172,22 @@ public function execute() } try { - //activate and send greeting email + // log in and send greeting email $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); - $this->customerAccountManagement->activate($customerEmail, $key); - $this->messageManager->addSuccess($this->getSuccessMessage()); + $customer = $this->customerAccountManagement->activate($customerEmail, $key); + $successMessage = $this->getSuccessMessage(); + $this->session->setCustomerDataAsLoggedIn($customer); + + if ($this->getCookieManager()->getCookie('mage-cache-sessid')) { + $metadata = $this->getCookieMetadataFactory()->createCookieMetadata(); + $metadata->setPath('/'); + $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata); + } + + if ($successMessage) { + $this->messageManager->addSuccess($successMessage); + } + $resultRedirect->setUrl($this->getSuccessRedirect()); return $resultRedirect; } catch (StateException $e) { @@ -135,33 +200,41 @@ public function execute() return $resultRedirect->setUrl($this->_redirect->error($url)); } + /** + * Returns customer id from request + * + * @return int + */ + private function getCustomerId(): int + { + return (int)$this->getRequest()->getParam('id', 0); + } + /** * Retrieve success message * - * @return string + * @return Phrase|null + * @throws NoSuchEntityException */ protected function getSuccessMessage() { if ($this->addressHelper->isVatValidationEnabled()) { - if ($this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING) { - // @codingStandardsIgnoreStart - $message = __( - 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your shipping address for proper VAT calculation.', - $this->urlModel->getUrl('customer/address/edit') - ); - // @codingStandardsIgnoreEnd - } else { - // @codingStandardsIgnoreStart - $message = __( - 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your billing address for proper VAT calculation.', - $this->urlModel->getUrl('customer/address/edit') - ); - // @codingStandardsIgnoreEnd - } - } else { - $message = __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()); + return __( + $this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING + ? 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your ' + .'shipping address for proper VAT calculation.' + :'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your ' + .'billing address for proper VAT calculation.', + $this->urlModel->getUrl('customer/address/edit') + ); } - return $message; + + $customerId = $this->getCustomerId(); + if ($customerId && $this->customerLogger->get($customerId)->getLastLoginAt()) { + return null; + } + + return __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()); } /** diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index d616c03be6bd0..085b4ab2d3fd9 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,7 +9,9 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\SessionCleanerInterface; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Url; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\Customer\Mapper; @@ -27,10 +28,12 @@ use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; use Magento\Framework\Escaper; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\SessionException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Customer\Controller\AbstractAccount; use Magento\Framework\Phrase; @@ -41,18 +44,19 @@ * Customer edit account information controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface { /** * Form code for data extractor */ - const FORM_DATA_EXTRACTOR_CODE = 'customer_account_edit'; + public const FORM_DATA_EXTRACTOR_CODE = 'customer_account_edit'; /** * @var AccountManagementInterface */ - protected $customerAccountManagement; + protected $accountManagement; /** * @var CustomerRepositoryInterface @@ -105,37 +109,51 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http private $filesystem; /** - * @var SessionCleanerInterface|null + * @var SessionCleanerInterface */ private $sessionCleaner; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + + /** + * @var Url + */ + private Url $customerUrl; + /** * @param Context $context * @param Session $customerSession - * @param AccountManagementInterface $customerAccountManagement + * @param AccountManagementInterface $accountManagement * @param CustomerRepositoryInterface $customerRepository * @param Validator $formKeyValidator * @param CustomerExtractor $customerExtractor * @param Escaper|null $escaper * @param AddressRegistry|null $addressRegistry - * @param Filesystem $filesystem + * @param Filesystem|null $filesystem * @param SessionCleanerInterface|null $sessionCleaner + * @param AccountConfirmation|null $accountConfirmation + * @param Url|null $customerUrl */ public function __construct( Context $context, Session $customerSession, - AccountManagementInterface $customerAccountManagement, + AccountManagementInterface $accountManagement, CustomerRepositoryInterface $customerRepository, Validator $formKeyValidator, CustomerExtractor $customerExtractor, ?Escaper $escaper = null, - AddressRegistry $addressRegistry = null, - Filesystem $filesystem = null, - ?SessionCleanerInterface $sessionCleaner = null + ?AddressRegistry $addressRegistry = null, + ?Filesystem $filesystem = null, + ?SessionCleanerInterface $sessionCleaner = null, + ?AccountConfirmation $accountConfirmation = null, + ?Url $customerUrl = null ) { parent::__construct($context); $this->session = $customerSession; - $this->customerAccountManagement = $customerAccountManagement; + $this->accountManagement = $accountManagement; $this->customerRepository = $customerRepository; $this->formKeyValidator = $formKeyValidator; $this->customerExtractor = $customerExtractor; @@ -143,6 +161,9 @@ public function __construct( $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); $this->sessionCleaner = $sessionCleaner ?: ObjectManager::getInstance()->get(SessionCleanerInterface::class); + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); + $this->customerUrl = $customerUrl ?: ObjectManager::getInstance()->get(Url::class); } /** @@ -164,7 +185,6 @@ private function getAuthentication() * Get email notification * * @return EmailNotificationInterface - * @deprecated 100.1.0 */ private function getEmailNotification() { @@ -180,7 +200,6 @@ private function getEmailNotification() */ public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException { - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/edit'); @@ -203,50 +222,49 @@ public function validateForCsrf(RequestInterface $request): ?bool * * @return Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws SessionException */ public function execute() { - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $validFormKey = $this->formKeyValidator->validate($this->getRequest()); if ($validFormKey && $this->getRequest()->isPost()) { - $currentCustomerDataObject = $this->getCustomerDataObject($this->session->getCustomerId()); - $customerCandidateDataObject = $this->populateNewCustomerDataObject( - $this->_request, - $currentCustomerDataObject - ); + $customer = $this->getCustomerDataObject($this->session->getCustomerId()); + $customerCandidate = $this->populateNewCustomerDataObject($this->_request, $customer); $attributeToDelete = $this->_request->getParam('delete_attribute_value'); if ($attributeToDelete !== null) { - $this->deleteCustomerFileAttribute( - $customerCandidateDataObject, - $attributeToDelete - ); + $this->deleteCustomerFileAttribute($customerCandidate, $attributeToDelete); } try { // whether a customer enabled change email option - $isEmailChanged = $this->processChangeEmailRequest($currentCustomerDataObject); + $isEmailChanged = $this->processChangeEmailRequest($customer); // whether a customer enabled change password option - $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); + $isPasswordChanged = $this->changeCustomerPassword($customer->getEmail()); // No need to validate customer address while editing customer profile - $this->disableAddressValidation($customerCandidateDataObject); + $this->disableAddressValidation($customerCandidate); + + $this->customerRepository->save($customerCandidate); + $updatedCustomer = $this->customerRepository->getById($customerCandidate->getId()); - $this->customerRepository->save($customerCandidateDataObject); $this->getEmailNotification()->credentialsChanged( - $customerCandidateDataObject, - $currentCustomerDataObject->getEmail(), + $updatedCustomer, + $customer->getEmail(), $isPasswordChanged ); - $this->dispatchSuccessEvent($customerCandidateDataObject); + + $this->dispatchSuccessEvent($updatedCustomer); $this->messageManager->addSuccessMessage(__('You saved the account information.')); // logout from current session if password or email changed. if ($isPasswordChanged || $isEmailChanged) { $this->session->logout(); $this->session->start(); + $this->addComplexSuccessMessage($customer, $updatedCustomer); + return $resultRedirect->setPath('customer/account/login'); } return $resultRedirect->setPath('customer/account'); @@ -276,13 +294,32 @@ public function execute() $this->session->setCustomerFormData($this->getRequest()->getPostValue()); } - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/edit'); return $resultRedirect; } + /** + * Adds a complex success message if email confirmation is required + * + * @param CustomerInterface $outdatedCustomer + * @param CustomerInterface $updatedCustomer + * @throws LocalizedException + */ + private function addComplexSuccessMessage( + CustomerInterface $outdatedCustomer, + CustomerInterface $updatedCustomer + ): void { + if (($outdatedCustomer->getEmail() !== $updatedCustomer->getEmail()) + && $this->accountConfirmation->isCustomerEmailChangedConfirmRequired($updatedCustomer)) { + $this->messageManager->addComplexSuccessMessage( + 'confirmAccountSuccessMessage', + ['url' => $this->customerUrl->getEmailConfirmationUrl($updatedCustomer->getEmail())] + ); + } + } + /** * Account editing action completed successfully event * @@ -303,6 +340,8 @@ private function dispatchSuccessEvent(CustomerInterface $customerCandidateDataOb * @param int $customerId * * @return CustomerInterface + * @throws LocalizedException + * @throws NoSuchEntityException */ private function getCustomerDataObject($customerId) { @@ -342,7 +381,7 @@ private function populateNewCustomerDataObject( * * @param string $email * @return boolean - * @throws InvalidEmailOrPasswordException|InputException + * @throws InvalidEmailOrPasswordException|InputException|LocalizedException */ protected function changeCustomerPassword($email) { @@ -355,7 +394,7 @@ protected function changeCustomerPassword($email) throw new InputException(__('Password confirmation doesn\'t match entered password.')); } - $isPasswordChanged = $this->customerAccountManagement->changePassword($email, $currPass, $newPass); + $isPasswordChanged = $this->accountManagement->changePassword($email, $currPass, $newPass); } return $isPasswordChanged; @@ -393,8 +432,6 @@ private function processChangeEmailRequest(CustomerInterface $currentCustomerDat * Get Customer Mapper instance * * @return Mapper - * - * @deprecated 100.1.3 */ private function getCustomerMapper() { @@ -424,6 +461,7 @@ private function disableAddressValidation($customer) * @param CustomerInterface $customerCandidateDataObject * @param string $attributeToDelete * @return void + * @throws FileSystemException */ private function deleteCustomerFileAttribute( CustomerInterface $customerCandidateDataObject, diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 4c07864f9b957..da70e7e10bd44 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -91,8 +91,7 @@ public function execute() ? [] : $this->getRequest()->getParam('customer_group_excluded_websites'); $resultRedirect = $this->resultRedirectFactory->create(); try { - $customerGroupCode = (string)$this->getRequest()->getParam('code'); - + $customerGroupCode = trim((string)$this->getRequest()->getParam('code')); if ($id !== null) { $customerGroup = $this->groupRepository->getById((int)$id); $customerGroupCode = $customerGroupCode ?: $customerGroup->getCode(); diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 74eee759b4abd..020b1799bb2c0 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Directory\Model\Country\Format; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\View\Element\BlockInterface; use Magento\Store\Model\ScopeInterface; @@ -19,28 +20,31 @@ * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting */ -class Address extends \Magento\Framework\App\Helper\AbstractHelper +class Address extends \Magento\Framework\App\Helper\AbstractHelper implements ResetAfterRequestInterface { /** * VAT Validation parameters XML paths */ - const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = 'customer/create_account/viv_disable_auto_group_assign_default'; + public const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = + 'customer/create_account/viv_disable_auto_group_assign_default'; - const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction'; + public const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction'; - const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign'; + public const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign'; - const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type'; + public const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type'; - const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility'; + public const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility'; /** * Possible customer address types */ - const TYPE_BILLING = 'billing'; + public const TYPE_BILLING = 'billing'; - const TYPE_SHIPPING = 'shipping'; + public const TYPE_SHIPPING = 'shipping'; /** * Array of Customer Address Attributes @@ -82,6 +86,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper * @var CustomerMetadataInterface * * @deprecated 101.0.0 + * phpcs:disable Magento2.Annotation.ClassPropertyPHPDocFormatting */ protected $_customerMetadataService; @@ -161,7 +166,7 @@ public function getCreateUrl() * Retrieve block renderer. * * @param string $renderer - * @return \Magento\Framework\View\Element\BlockInterface + * @return BlockInterface */ public function getRenderer($renderer) { @@ -281,7 +286,7 @@ public function getAttributeValidationClass($attributeCode) : $this->_addressMetadataService->getAttributeMetadata($attributeCode); $class = $attribute ? $attribute->getFrontendClass() : ''; - } catch (NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // the attribute does not exist so just return an empty string } @@ -417,4 +422,14 @@ public function isAttributeVisible($code) } return false; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_config = []; + $this->_attributes = []; + $this->_streetLines = []; + } } diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php index f5193bc50026f..d95308e4fbe2a 100644 --- a/app/code/Magento/Customer/Model/AccountConfirmation.php +++ b/app/code/Magento/Customer/Model/AccountConfirmation.php @@ -3,8 +3,11 @@ * 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\CustomerInterface; use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Registry; @@ -15,10 +18,30 @@ class AccountConfirmation { /** - * Configuration path for email confirmation. + * Configuration path for email confirmation when creating a new customer */ public const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; + /** + * Configuration path for email confirmation when updating an existing customer's email + */ + public const XML_PATH_IS_CONFIRM_EMAIL_CHANGED = 'customer/account_information/confirm'; + + /** + * Constant for confirmed status + */ + private const ACCOUNT_CONFIRMED = 'account_confirmed'; + + /** + * Constant for confirmation required status + */ + private const ACCOUNT_CONFIRMATION_REQUIRED = 'account_confirmation_required'; + + /** + * Constant for confirmation not required status + */ + private const ACCOUNT_CONFIRMATION_NOT_REQUIRED = 'account_confirmation_not_required'; + /** * @var ScopeConfigInterface */ @@ -64,6 +87,54 @@ public function isConfirmationRequired($websiteId, $customerId, $customerEmail): ); } + /** + * Check if accounts confirmation is required if email has been changed + * + * @param int|null $websiteId + * @param int|null $customerId + * @param string|null $customerEmail + * @return bool + */ + public function isEmailChangedConfirmationRequired($websiteId, $customerId, $customerEmail): bool + { + return !$this->canSkipConfirmation($customerId, $customerEmail) + && $this->scopeConfig->isSetFlag( + self::XML_PATH_IS_CONFIRM_EMAIL_CHANGED, + ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); + } + + /** + * Returns an email confirmation status if email has been changed + * + * @param CustomerInterface $customer + * @return string + */ + private function getEmailChangedConfirmStatus(CustomerInterface $customer): string + { + $isEmailChangedConfirmationRequired = $this->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + + return $isEmailChangedConfirmationRequired + ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED + : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; + } + + /** + * Checks if email confirmation is required for the customer + * + * @param CustomerInterface $customer + * @return bool + */ + public function isCustomerEmailChangedConfirmRequired(CustomerInterface $customer):bool + { + return $this->getEmailChangedConfirmStatus($customer) === self::ACCOUNT_CONFIRMATION_REQUIRED; + } + /** * Check whether confirmation may be skipped when registering using certain email address. * diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index d5689fd2b8c08..29f1ebc0e531b 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Model; @@ -19,6 +20,7 @@ use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\CredentialsValidator; use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use Magento\Customer\Model\Logger as CustomerLogger; use Magento\Customer\Model\Metadata\Validator; use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; use Magento\Directory\Model\AllowedCountries; @@ -67,6 +69,11 @@ */ class AccountManagement implements AccountManagementInterface { + /** + * System Configuration Path for Enable/Disable Login at Guest Checkout + */ + public const GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG = 'checkout/options/enable_guest_checkout_login'; + /** * Configuration paths for create account email template * @@ -219,7 +226,7 @@ class AccountManagement implements AccountManagementInterface private $customerFactory; /** - * @var \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory + * @var ValidationResultsInterfaceFactory */ private $validationResultsDataFactory; @@ -229,7 +236,7 @@ class AccountManagement implements AccountManagementInterface private $eventManager; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; @@ -299,7 +306,7 @@ class AccountManagement implements AccountManagementInterface protected $dataProcessor; /** - * @var \Magento\Framework\Registry + * @var Registry */ protected $registry; @@ -319,7 +326,7 @@ class AccountManagement implements AccountManagementInterface protected $objectFactory; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; @@ -339,7 +346,7 @@ class AccountManagement implements AccountManagementInterface private $emailNotification; /** - * @var \Magento\Eav\Model\Validator\Attribute\Backend + * @var Backend */ private $eavValidator; @@ -388,6 +395,11 @@ class AccountManagement implements AccountManagementInterface */ private $authorization; + /** + * @var CustomerLogger + */ + private CustomerLogger $customerLogger; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -426,6 +438,7 @@ class AccountManagement implements AccountManagementInterface * @param AuthorizationInterface|null $authorization * @param AuthenticationInterface|null $authentication * @param Backend|null $eavValidator + * @param CustomerLogger|null $customerLogger * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -469,7 +482,8 @@ public function __construct( SessionCleanerInterface $sessionCleaner = null, AuthorizationInterface $authorization = null, AuthenticationInterface $authentication = null, - Backend $eavValidator = null + Backend $eavValidator = null, + ?CustomerLogger $customerLogger = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -512,6 +526,7 @@ public function __construct( $this->authorization = $authorization ?? $objectManager->get(AuthorizationInterface::class); $this->authentication = $authentication ?? $objectManager->get(AuthenticationInterface::class); $this->eavValidator = $eavValidator ?? $objectManager->get(Backend::class); + $this->customerLogger = $customerLogger ?? $objectManager->get(CustomerLogger::class); } /** @@ -562,9 +577,9 @@ public function activateById($customerId, $confirmationKey) /** * Activate a customer account using a key that was sent in a confirmation email. * - * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param CustomerInterface $customer * @param string $confirmationKey - * @return \Magento\Customer\Api\Data\CustomerInterface + * @return CustomerInterface * @throws InputException * @throws InputMismatchException * @throws InvalidTransitionException @@ -586,12 +601,17 @@ private function activateCustomer($customer, $confirmationKey) // No need to validate customer and customer address while activating customer $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); - $this->getEmailNotification()->newAccount( - $customer, - 'confirmed', - '', - $this->storeManager->getStore()->getId() - ); + + $customerLastLoginAt = $this->customerLogger->get((int)$customer->getId())->getLastLoginAt(); + if (!$customerLastLoginAt) { + $this->getEmailNotification()->newAccount( + $customer, + 'confirmed', + '', + $this->storeManager->getStore()->getId() + ); + } + return $customer; } @@ -615,7 +635,9 @@ public function authenticate($username, $password) } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException(__('Invalid login or password.')); } - if ($customer->getConfirmation() && $this->isConfirmationRequired($customer)) { + + if ($customer->getConfirmation() + && ($this->isConfirmationRequired($customer) || $this->isEmailChangedConfirmationRequired($customer))) { throw new EmailNotConfirmedException(__("This account isn't confirmed. Verify and try again.")); } @@ -630,6 +652,21 @@ public function authenticate($username, $password) return $customer; } + /** + * Checks if account confirmation is required if the email address has been changed + * + * @param CustomerInterface $customer + * @return bool + */ + private function isEmailChangedConfirmationRequired(CustomerInterface $customer): bool + { + return $this->accountConfirmation->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + } + /** * @inheritdoc */ @@ -687,7 +724,7 @@ private function handleUnknownTemplate($template) throw new InputException( __( 'Invalid value of "%value" provided for the %fieldName field. ' - . 'Possible values: %template1 or %template2.', + . 'Possible values: %template1 or %template2.', [ 'value' => $template, 'fieldName' => 'template', @@ -715,7 +752,7 @@ public function resetPassword($email, $resetToken, $newPassword) $this->setIgnoreValidationFlag($customer); //Validate Token and new password strength - $this->validateResetPasswordToken($customer->getId(), $resetToken); + $this->validateResetPasswordToken((int)$customer->getId(), $resetToken); $this->credentialsValidator->checkPasswordDifferentFromEmail( $email, $newPassword @@ -832,13 +869,10 @@ public function getConfirmationStatus($customerId) { // load customer by id $customer = $this->customerRepository->getById($customerId); - if ($this->isConfirmationRequired($customer)) { - if (!$customer->getConfirmation()) { - return self::ACCOUNT_CONFIRMED; - } - return self::ACCOUNT_CONFIRMATION_REQUIRED; - } - return self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; + + return $this->isConfirmationRequired($customer) + ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED + : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; } /** @@ -848,11 +882,6 @@ public function getConfirmationStatus($customerId) */ public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') { - $groupId = $customer->getGroupId(); - if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { - $customer->setGroupId(null); - } - if ($password !== null) { $this->checkPasswordStrength($password); $customerEmail = $customer->getEmail(); @@ -1096,7 +1125,7 @@ public function validate(CustomerInterface $customer) $result = $this->eavValidator->isValid($customerModel); if ($result === false && is_array($this->eavValidator->getMessages())) { return $validationResults->setIsValid(false)->setMessages( - // phpcs:ignore Magento2.Functions.DiscouragedFunction + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func_array( 'array_merge', array_values($this->eavValidator->getMessages()) @@ -1108,9 +1137,24 @@ public function validate(CustomerInterface $customer) /** * @inheritdoc + * + * @param string $customerEmail + * @param int|null $websiteId + * @return bool + * @throws LocalizedException */ public function isEmailAvailable($customerEmail, $websiteId = null) { + $guestLoginConfig = $this->scopeConfig->getValue( + self::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + $websiteId + ); + + if (!$guestLoginConfig) { + return true; + } + try { if ($websiteId === null) { $websiteId = $this->storeManager->getStore()->getWebsiteId(); @@ -1219,7 +1263,7 @@ public function isReadonly($customerId) * @return $this * @throws LocalizedException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::newAccount() */ protected function sendNewAccountEmail( $customer, @@ -1263,7 +1307,7 @@ protected function sendNewAccountEmail( * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::credentialsChanged() */ protected function sendPasswordResetNotificationEmail($customer) { @@ -1277,7 +1321,7 @@ protected function sendPasswordResetNotificationEmail($customer) * @param int|string|null $defaultStoreId * @return int * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see StoreManagerInterface::getWebsite() * @throws LocalizedException */ protected function getWebsiteStoreId($customer, $defaultStoreId = null) @@ -1295,7 +1339,7 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null) * * @return array * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::TEMPLATE_TYPES */ protected function getTemplateTypes() { @@ -1329,7 +1373,7 @@ protected function getTemplateTypes() * @return $this * @throws MailException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::sendEmailTemplate() */ protected function sendEmailTemplate( $customer, @@ -1484,7 +1528,7 @@ public function changeResetPasswordLinkToken(CustomerInterface $customer, string * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::passwordReminder() */ public function sendPasswordReminderEmail($customer) { @@ -1514,7 +1558,7 @@ public function sendPasswordReminderEmail($customer) * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::passwordResetConfirmation() */ public function sendPasswordResetConfirmationEmail($customer) { @@ -1560,7 +1604,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId) * @return Data\CustomerSecure * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::getFullCustomerObject() */ protected function getFullCustomerObject($customer) { @@ -1569,7 +1613,7 @@ protected function getFullCustomerObject($customer) $mergedCustomerData = $this->customerRegistry->retrieveSecureData($customer->getId()); $customerData = $this->dataProcessor->buildOutputDataArray( $customer, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $mergedCustomerData->addData($customerData); $mergedCustomerData->setData('name', $this->customerViewHelper->getCustomerName($customer)); @@ -1605,8 +1649,6 @@ private function disableAddressValidation($customer) * Get email notification * * @return EmailNotificationInterface - * @deprecated 100.1.0 - * @see MAGETWO-71174 */ private function getEmailNotification() { diff --git a/app/code/Magento/Customer/Model/AccountManagementApi.php b/app/code/Magento/Customer/Model/AccountManagementApi.php index 02a05705b57ef..8b4f78ab26c77 100644 --- a/app/code/Magento/Customer/Model/AccountManagementApi.php +++ b/app/code/Magento/Customer/Model/AccountManagementApi.php @@ -6,16 +6,127 @@ namespace Magento\Customer\Model; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Helper\View as CustomerViewHelper; +use Magento\Customer\Model\Config\Share as ConfigShare; +use Magento\Customer\Model\Customer as CustomerModel; +use Magento\Customer\Model\Metadata\Validator; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\DataObjectFactory as ObjectFactory; +use Magento\Framework\Encryption\EncryptorInterface as Encryptor; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Math\Random; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\StringUtils as StringHelper; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface as PsrLogger; /** * Account Management service implementation for external API access. + * * Handle various customer account actions. * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountManagementApi extends AccountManagement { + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @param CustomerFactory $customerFactory + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param Random $mathRandom + * @param Validator $validator + * @param ValidationResultsInterfaceFactory $validationResultsDataFactory + * @param AddressRepositoryInterface $addressRepository + * @param CustomerMetadataInterface $customerMetadataService + * @param CustomerRegistry $customerRegistry + * @param PsrLogger $logger + * @param Encryptor $encryptor + * @param ConfigShare $configShare + * @param StringHelper $stringHelper + * @param CustomerRepositoryInterface $customerRepository + * @param ScopeConfigInterface $scopeConfig + * @param TransportBuilder $transportBuilder + * @param DataObjectProcessor $dataProcessor + * @param Registry $registry + * @param CustomerViewHelper $customerViewHelper + * @param DateTime $dateTime + * @param CustomerModel $customerModel + * @param ObjectFactory $objectFactory + * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param AuthorizationInterface $authorization + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + CustomerFactory $customerFactory, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + Random $mathRandom, + Validator $validator, + ValidationResultsInterfaceFactory $validationResultsDataFactory, + AddressRepositoryInterface $addressRepository, + CustomerMetadataInterface $customerMetadataService, + CustomerRegistry $customerRegistry, + PsrLogger $logger, + Encryptor $encryptor, + ConfigShare $configShare, + StringHelper $stringHelper, + CustomerRepositoryInterface $customerRepository, + ScopeConfigInterface $scopeConfig, + TransportBuilder $transportBuilder, + DataObjectProcessor $dataProcessor, + Registry $registry, + CustomerViewHelper $customerViewHelper, + DateTime $dateTime, + CustomerModel $customerModel, + ObjectFactory $objectFactory, + ExtensibleDataObjectConverter $extensibleDataObjectConverter, + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + parent::__construct( + $customerFactory, + $eventManager, + $storeManager, + $mathRandom, + $validator, + $validationResultsDataFactory, + $addressRepository, + $customerMetadataService, + $customerRegistry, + $logger, + $encryptor, + $configShare, + $stringHelper, + $customerRepository, + $scopeConfig, + $transportBuilder, + $dataProcessor, + $registry, + $customerViewHelper, + $dateTime, + $customerModel, + $objectFactory, + $extensibleDataObjectConverter + ); + } + /** * @inheritDoc * @@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement */ public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') { + $this->validateCustomerRequest($customer); $customer = parent::createAccount($customer, $password, $redirectUrl); $customer->setConfirmation(null); return $customer; } + + /** + * Validate anonymous request + * + * @param CustomerInterface $customer + * @return void + * @throws AuthorizationException + */ + private function validateCustomerRequest(CustomerInterface $customer): void + { + $groupId = $customer->getGroupId(); + if (isset($groupId) && + !$this->authorization->isAllowed(self::ADMIN_RESOURCE) + ) { + $params = ['resources' => self::ADMIN_RESOURCE]; + throw new AuthorizationException( + __("The consumer isn't authorized to access %resources.", $params) + ); + } + } } diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index f710ef6846fd6..9d55ada6d0ac5 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -14,6 +14,7 @@ use Magento\Customer\Model\Data\Address as AddressData; use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Address abstract model @@ -31,11 +32,12 @@ * @method string getPostcode() * @method bool getShouldIgnoreValidation() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * * @api * @since 100.0.2 */ -class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface +class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface, ResetAfterRequestInterface { /** * Possible customer address types @@ -336,7 +338,7 @@ protected function _implodeArrayValues($value) $isScalar = true; foreach ($value as $val) { - if (!is_scalar($val)) { + if ($val !== null && !is_scalar($val)) { $isScalar = false; break; } @@ -736,4 +738,13 @@ private function processCustomAttribute(array $attribute): array return $attribute; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_countryModels = []; + self::$_regionModels = []; + } } diff --git a/app/code/Magento/Customer/Model/AddressRegistry.php b/app/code/Magento/Customer/Model/AddressRegistry.php index 1fed9d5b6b545..d29e42c1e03d8 100644 --- a/app/code/Magento/Customer/Model/AddressRegistry.php +++ b/app/code/Magento/Customer/Model/AddressRegistry.php @@ -7,11 +7,12 @@ namespace Magento\Customer\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Registry for Address models */ -class AddressRegistry +class AddressRegistry implements ResetAfterRequestInterface { /** * @var Address[] @@ -74,4 +75,12 @@ public function push(Address $address) $this->registry[$address->getId()] = $address; return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->registry = []; + } } diff --git a/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php b/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php deleted file mode 100644 index 53e170f6026f8..0000000000000 --- a/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Customer\Model\App\FrontController; - -use Magento\Framework\App\Response\Http as ResponseHttp; -use Magento\Customer\Model\Session; - -/** - * Plugin for delete the cookie when the customer is not exist. - * - * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ -class DeleteCookieWhenCustomerNotExistPlugin -{ - /** - * @var ResponseHttp - */ - private $responseHttp; - - /** - * @var Session - */ - private $session; - - /** - * Constructor - * - * @param ResponseHttp $responseHttp - * @param Session $session - */ - public function __construct( - ResponseHttp $responseHttp, - Session $session - ) { - $this->responseHttp = $responseHttp; - $this->session = $session; - } - - /** - * Delete the cookie when the customer is not exist before dispatch the front controller. - * - * @return void - */ - public function beforeDispatch(): void - { - if (!$this->session->getCustomerId()) { - $this->responseHttp->sendVary(); - } - } -} diff --git a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php index f4418c2832855..95db7353758ad 100644 --- a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php +++ b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model\Config\Backend\Show; +use Magento\Config\App\Config\Source\ModularConfigSource; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; /** * Customer Show Customer Model * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.UnusedPrivateField) */ class Customer extends \Magento\Framework\App\Config\Value { @@ -32,6 +37,11 @@ class Customer extends \Magento\Framework\App\Config\Value */ private $telephoneShowDefaultValue = 'req'; + /** + * @var ModularConfigSource + */ + private $configSource; + /** * @var array */ @@ -52,6 +62,8 @@ class Customer extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ModularConfigSource|null $configSource + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -62,11 +74,13 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ModularConfigSource $configSource = null ) { $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); $this->storeManager = $storeManager; + $this->configSource = $configSource ?: ObjectManager::getInstance()->get(ModularConfigSource::class); } /** @@ -140,7 +154,8 @@ public function afterDelete() $attributeObject->save(); } } elseif ($this->getScope() == ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { - $valueConfig = $this->getValueConfig($this->telephoneShowDefaultValue); + $defaultValue = $this->configSource->get(ScopeConfigInterface::SCOPE_TYPE_DEFAULT . '/' . $this->getPath()); + $valueConfig = $this->getValueConfig($defaultValue === [] ? '' : $defaultValue); foreach ($this->_getAttributeObjects() as $attributeObject) { $attributeObject->setData('is_required', $valueConfig['is_required']); $attributeObject->setData('is_visible', $valueConfig['is_visible']); diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 42b0f86ec6cbb..d42a8b74734e9 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -1308,7 +1308,7 @@ public function isResetPasswordLinkTokenExpired() } $hourDifference = floor(($currentTimestamp - $tokenTimestamp) / (60 * 60)); - + return $hourDifference >= $expirationPeriod; } diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index 0f421c1c677ce..f05c0948ac07a 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -10,6 +10,7 @@ use Magento\Customer\Model\Data\CustomerSecure; use Magento\Customer\Model\Data\CustomerSecureFactory; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,9 +18,9 @@ * * @api */ -class CustomerRegistry +class CustomerRegistry implements ResetAfterRequestInterface { - const REGISTRY_SEPARATOR = ':'; + public const REGISTRY_SEPARATOR = ':'; /** * @var CustomerFactory @@ -116,9 +117,7 @@ public function retrieveByEmail($customerEmail, $websiteId = null) /** @var Customer $customer */ $customer = $this->customerFactory->create(); - if (isset($websiteId)) { - $customer->setWebsiteId($websiteId); - } + $customer->setWebsiteId($websiteId); $customer->loadByEmail($customerEmail); if (!$customer->getEmail()) { @@ -234,4 +233,14 @@ public function push(Customer $customer) $this->customerRegistryByEmail[$emailKey] = $customer; return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerRegistryById = []; + $this->customerRegistryByEmail = []; + $this->customerSecureRegistryById = []; + } } diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index a4f85a9c4a0c9..a71cf79a4f51b 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Store\Model\App\Emulation; use Magento\Store\Model\StoreManagerInterface; @@ -30,28 +32,28 @@ class EmailNotification implements EmailNotificationInterface /**#@+ * Configuration paths for email templates and identities */ - const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; + public const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; - const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; + public const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; - const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template'; + public const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template'; - const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE = + public const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE = 'customer/account_information/change_email_and_password_template'; - const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; + public const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; - const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; + public const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; - const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; + public const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; - const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; + public const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; - const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; + public const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; - const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; + public const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; - const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; + public const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; /** * self::NEW_ACCOUNT_EMAIL_REGISTERED welcome email, when confirmation is disabled @@ -62,7 +64,7 @@ class EmailNotification implements EmailNotificationInterface * and password is set * self::NEW_ACCOUNT_EMAIL_CONFIRMATION email with confirmation link */ - const TEMPLATE_TYPES = [ + public const TEMPLATE_TYPES = [ self::NEW_ACCOUNT_EMAIL_REGISTERED => self::XML_PATH_REGISTER_EMAIL_TEMPLATE, self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD => self::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE, self::NEW_ACCOUNT_EMAIL_CONFIRMED => self::XML_PATH_CONFIRMED_EMAIL_TEMPLATE, @@ -71,7 +73,9 @@ class EmailNotification implements EmailNotificationInterface /**#@-*/ - /**#@-*/ + /** + * @var CustomerRegistry + */ private $customerRegistry; /** @@ -109,6 +113,11 @@ class EmailNotification implements EmailNotificationInterface */ private $emulation; + /** + * @var AccountConfirmation + */ + private AccountConfirmation $accountConfirmation; + /** * @param CustomerRegistry $customerRegistry * @param StoreManagerInterface $storeManager @@ -118,6 +127,7 @@ class EmailNotification implements EmailNotificationInterface * @param ScopeConfigInterface $scopeConfig * @param SenderResolverInterface|null $senderResolver * @param Emulation|null $emulation + * @param AccountConfirmation|null $accountConfirmation */ public function __construct( CustomerRegistry $customerRegistry, @@ -127,7 +137,8 @@ public function __construct( DataObjectProcessor $dataProcessor, ScopeConfigInterface $scopeConfig, SenderResolverInterface $senderResolver = null, - Emulation $emulation =null + Emulation $emulation = null, + ?AccountConfirmation $accountConfirmation = null ) { $this->customerRegistry = $customerRegistry; $this->storeManager = $storeManager; @@ -137,6 +148,8 @@ public function __construct( $this->scopeConfig = $scopeConfig; $this->senderResolver = $senderResolver ?? ObjectManager::getInstance()->get(SenderResolverInterface::class); $this->emulation = $emulation ?? ObjectManager::getInstance()->get(Emulation::class); + $this->accountConfirmation = $accountConfirmation ?? ObjectManager::getInstance() + ->get(AccountConfirmation::class); } /** @@ -146,6 +159,7 @@ public function __construct( * @param string $origCustomerEmail * @param bool $isPasswordChanged * @return void + * @throws LocalizedException */ public function credentialsChanged( CustomerInterface $savedCustomer, @@ -153,6 +167,7 @@ public function credentialsChanged( $isPasswordChanged = false ): void { if ($origCustomerEmail != $savedCustomer->getEmail()) { + $this->emailChangedConfirmation($savedCustomer); if ($isPasswordChanged) { $this->emailAndPasswordChanged($savedCustomer, $origCustomerEmail); $this->emailAndPasswordChanged($savedCustomer, $savedCustomer->getEmail()); @@ -175,6 +190,8 @@ public function credentialsChanged( * @param CustomerInterface $customer * @param string $email * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function emailAndPasswordChanged(CustomerInterface $customer, $email): void { @@ -201,6 +218,8 @@ private function emailAndPasswordChanged(CustomerInterface $customer, $email): v * @param CustomerInterface $customer * @param string $email * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function emailChanged(CustomerInterface $customer, $email): void { @@ -226,6 +245,8 @@ private function emailChanged(CustomerInterface $customer, $email): void * * @param CustomerInterface $customer * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function passwordReset(CustomerInterface $customer): void { @@ -255,7 +276,7 @@ private function passwordReset(CustomerInterface $customer): void * @param int|null $storeId * @param string $email * @return void - * @throws \Magento\Framework\Exception\MailException + * @throws MailException|LocalizedException */ private function sendEmailTemplate( $customer, @@ -293,6 +314,7 @@ private function sendEmailTemplate( * * @param CustomerInterface $customer * @return CustomerSecure + * @throws NoSuchEntityException */ private function getFullCustomerObject($customer): CustomerSecure { @@ -312,6 +334,7 @@ private function getFullCustomerObject($customer): CustomerSecure * @param CustomerInterface $customer * @param int|string|null $defaultStoreId * @return int + * @throws LocalizedException */ private function getWebsiteStoreId($customer, $defaultStoreId = null): int { @@ -327,6 +350,9 @@ private function getWebsiteStoreId($customer, $defaultStoreId = null): int * * @param CustomerInterface $customer * @return void + * @throws LocalizedException + * @throws MailException + * @throws NoSuchEntityException */ public function passwordReminder(CustomerInterface $customer): void { @@ -351,6 +377,9 @@ public function passwordReminder(CustomerInterface $customer): void * * @param CustomerInterface $customer * @return void + * @throws LocalizedException + * @throws MailException + * @throws NoSuchEntityException */ public function passwordResetConfirmation(CustomerInterface $customer): void { @@ -412,4 +441,18 @@ public function newAccount( $storeId ); } + + /** + * Sending an email to confirm the email address in case the email address has been changed + * + * @param CustomerInterface $customer + * @throws LocalizedException + */ + private function emailChangedConfirmation(CustomerInterface $customer): void + { + if (!$this->accountConfirmation->isCustomerEmailChangedConfirmRequired($customer)) { + return; + } + $this->newAccount($customer, self::NEW_ACCOUNT_EMAIL_CONFIRMATION, null, $customer->getStoreId()); + } } diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php index 8e64fba4a9b08..23ce32b9e217d 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php @@ -11,18 +11,19 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\CacheInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\StoreManagerInterface; /** * Cache for attribute metadata */ -class AttributeMetadataCache +class AttributeMetadataCache implements ResetAfterRequestInterface { /** * Cache prefix */ - const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE'; + public const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE'; /** * @var CacheInterface @@ -155,7 +156,7 @@ public function clean() $this->cache->clean( [ Type::CACHE_TAG, - Attribute::CACHE_TAG, + Attribute::CACHE_TAG ] ); } @@ -173,4 +174,12 @@ private function isEnabled() } return $this->isAttributeCacheEnabled; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = []; + } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 54b8b75c9ca3e..05788dcaf763c 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -23,6 +23,8 @@ */ class File extends AbstractData { + public const UPLOADED_FILE_SUFFIX = '_uploaded'; + /** * Validator for check not protected extensions * @@ -59,7 +61,8 @@ class File extends AbstractData /** * @var FileProcessorFactory - * @deprecated 101.0.0 + * @deprecated 101.0.0 Call fileProcessor directly from code + * @see $this->fileProcessor */ protected $fileProcessorFactory; @@ -126,7 +129,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) $attrCode = $this->getAttribute()->getAttributeCode(); // phpcs:disable Magento2.Security.Superglobal - $uploadedFile = $request->getParam($attrCode . '_uploaded'); + $uploadedFile = $request->getParam($attrCode . static::UPLOADED_FILE_SUFFIX); if ($uploadedFile) { $value = $uploadedFile; } elseif ($this->_requestScope || !isset($_FILES[$attrCode])) { @@ -424,7 +427,8 @@ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFa * Get file processor * * @return FileProcessor - * @deprecated 100.1.3 + * @deprecated 100.1.3 we don’t use such approach anymore. Call fileProcessor directly + * @see $this->fileProcessor */ protected function getFileProcessor() { diff --git a/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php new file mode 100644 index 0000000000000..ec837d9737595 --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Plugin; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\StorageInterface; +use Magento\Framework\Exception\SessionException; +use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Clears previous active sessions after logout + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class ClearSessionsAfterLogoutPlugin +{ + /** + * Array key for all active previous session ids. + */ + private const PREVIOUS_ACTIVE_SESSIONS = 'previous_active_sessions'; + + /** + * @var Session + */ + private Session $session; + + /** + * @var SaveHandlerInterface + */ + private SaveHandlerInterface $saveHandler; + + /** + * @var StorageInterface + */ + private StorageInterface $storage; + + /** + * @var State + */ + private State $state; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * Initialize Dependencies + * + * @param Session $customerSession + * @param SaveHandlerInterface $saveHandler + * @param StorageInterface $storage + * @param State $state + * @param LoggerInterface $logger + */ + public function __construct( + Session $customerSession, + SaveHandlerInterface $saveHandler, + StorageInterface $storage, + State $state, + LoggerInterface $logger + ) { + $this->session = $customerSession; + $this->saveHandler = $saveHandler; + $this->storage = $storage; + $this->state = $state; + $this->logger = $logger; + } + + /** + * Plugin to clear session after logout + * + * @param Session $subject + * @param Session $result + * @return Session + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterLogout(Session $subject, Session $result): Session + { + $isAreaFrontEnd = $this->state->getAreaCode() === Area::AREA_FRONTEND; + $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS); + + if ($isAreaFrontEnd && !empty($previousSessions)) { + foreach ($previousSessions as $sessionId) { + try { + $this->session->start(); + $this->saveHandler->destroy($sessionId); + $this->session->writeClose(); + } catch (SessionException $e) { + $this->logger->error($e); + } + + } + $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, []); + } + return $result; + } +} diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php index db694ad3295ce..ab3cf8cb7d852 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Model\Plugin; @@ -16,13 +17,21 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\App\State; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Session\StorageInterface; use Psr\Log\LoggerInterface; /** * Refresh the Customer session if `UpdateSession` notification registered + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomerNotification { + /** + * Array key for all active previous session ids. + */ + private const PREVIOUS_ACTIVE_SESSIONS = 'previous_active_sessions'; + /** * @var Session */ @@ -53,6 +62,11 @@ class CustomerNotification */ private $request; + /** + * @var StorageInterface + */ + private StorageInterface $storage; + /** * Initialize dependencies. * @@ -61,7 +75,8 @@ class CustomerNotification * @param State $state * @param CustomerRepositoryInterface $customerRepository * @param LoggerInterface $logger - * @param RequestInterface|null $request + * @param RequestInterface $request + * @param StorageInterface|null $storage */ public function __construct( Session $session, @@ -69,7 +84,8 @@ public function __construct( State $state, CustomerRepositoryInterface $customerRepository, LoggerInterface $logger, - RequestInterface $request + RequestInterface $request, + StorageInterface $storage = null ) { $this->session = $session; $this->notificationStorage = $notificationStorage; @@ -77,6 +93,7 @@ public function __construct( $this->customerRepository = $customerRepository; $this->logger = $logger; $this->request = $request; + $this->storage = $storage ?? ObjectManager::getInstance()->get(StorageInterface::class); } /** @@ -89,18 +106,33 @@ public function __construct( */ public function beforeExecute(ActionInterface $subject) { - $customerId = $this->session->getCustomerId(); - - if ($this->isFrontendRequest() && $this->isPostRequest() && $this->isSessionUpdateRegisteredFor($customerId)) { - try { - $this->session->regenerateId(); - $customer = $this->customerRepository->getById($customerId); - $this->session->setCustomerData($customer); - $this->session->setCustomerGroupId($customer->getGroupId()); - $this->notificationStorage->remove(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customer->getId()); - } catch (NoSuchEntityException $e) { - $this->logger->error($e); + $customerId = (int)$this->session->getCustomerId(); + + if (!$this->isFrontendRequest() + || !$this->isPostRequest() + || !$this->isSessionUpdateRegisteredFor($customerId)) { + return; + } + + try { + $oldSessionId = $this->session->getSessionId(); + $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS); + + if (empty($previousSessions)) { + $previousSessions = []; } + $previousSessions[] = $oldSessionId; + $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, $previousSessions); + $this->session->regenerateId(); + $customer = $this->customerRepository->getById($customerId); + $this->session->setCustomerData($customer); + $this->session->setCustomerGroupId($customer->getGroupId()); + $this->notificationStorage->remove( + NotificationStorage::UPDATE_CUSTOMER_SESSION, + $customer->getId() + ); + } catch (NoSuchEntityException $e) { + $this->logger->error($e); } } @@ -131,8 +163,8 @@ private function isFrontendRequest(): bool * @param int $customerId * @return bool */ - private function isSessionUpdateRegisteredFor($customerId): bool + private function isSessionUpdateRegisteredFor(int $customerId): bool { - return $this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId); + return (bool)$this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId); } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php index c7b44288bc85f..ffb5f41a40687 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php @@ -94,6 +94,15 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_idFieldName = 'entity_id'; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index c065f85aa6483..9ae14f68923fa 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -7,12 +7,25 @@ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\Customer\NotificationStorage; +use Magento\Eav\Model\Entity\Context; +use Magento\Eav\Model\Entity\VersionControl\AbstractEntity; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; +use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Validator\Exception as ValidatorException; use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Validator\Factory; +use Magento\Store\Model\StoreManagerInterface; /** * Customer entity resource model @@ -21,27 +34,27 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity +class Customer extends AbstractEntity { /** - * @var \Magento\Framework\Validator\Factory + * @var Factory */ protected $_validatorFactory; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $dateTime; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; @@ -63,26 +76,26 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity /** * Customer constructor. * - * @param \Magento\Eav\Model\Entity\Context $context - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Validator\Factory $validatorFactory - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Context $context + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite + * @param ScopeConfigInterface $scopeConfig + * @param Factory $validatorFactory + * @param DateTime $dateTime + * @param StoreManagerInterface $storeManager * @param array $data - * @param AccountConfirmation $accountConfirmation + * @param AccountConfirmation|null $accountConfirmation * @param EncryptorInterface|null $encryptor * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Eav\Model\Entity\Context $context, - \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\Validator\Factory $validatorFactory, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Store\Model\StoreManagerInterface $storeManager, + Context $context, + Snapshot $entitySnapshot, + RelationComposite $entityRelationComposite, + ScopeConfigInterface $scopeConfig, + Factory $validatorFactory, + DateTime $dateTime, + StoreManagerInterface $storeManager, $data = [], AccountConfirmation $accountConfirmation = null, EncryptorInterface $encryptor = null @@ -120,16 +133,16 @@ protected function _getDefaultAttributes() /** * Check customer scope, email and confirmation key before saving * - * @param \Magento\Framework\DataObject|\Magento\Customer\Api\Data\CustomerInterface $customer + * @param DataObject|CustomerInterface $customer * * @return $this * @throws AlreadyExistsException * @throws ValidatorException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function _beforeSave(\Magento\Framework\DataObject $customer) + protected function _beforeSave(DataObject $customer) { /** @var \Magento\Customer\Model\Customer $customer */ if ($customer->getStoreId() === null) { @@ -169,13 +182,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) } // set confirmation key logic - if (!$customer->getId() && - $this->accountConfirmation->isConfirmationRequired( - $customer->getWebsiteId(), - $customer->getId(), - $customer->getEmail() - ) - ) { + if ($this->isConfirmationRequired($customer)) { $customer->setConfirmation($customer->getRandomConfirmationKey()); } // remove customer confirmation key from database, if empty @@ -195,6 +202,51 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) return $this; } + /** + * Checks if customer email verification is required + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isConfirmationRequired(DataObject $customer): bool + { + return $this->isNewCustomerConfirmationRequired($customer) + || $this->isExistingCustomerConfirmationRequired($customer); + } + + /** + * Checks if customer email verification is required for a new customer + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isNewCustomerConfirmationRequired(DataObject $customer): bool + { + return !$customer->getId() + && $this->accountConfirmation->isConfirmationRequired( + $customer->getWebsiteId(), + $customer->getId(), + $customer->getEmail() + ); + } + + /** + * Checks if customer email verification is required for an existing customer + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isExistingCustomerConfirmationRequired(DataObject $customer): bool + { + return $customer->getId() + && $customer->dataHasChangedFor('email') + && $this->accountConfirmation->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + } + /** * Validate customer entity * @@ -231,10 +283,10 @@ private function getNotificationStorage() /** * Save customer addresses and set default addresses in attributes backend * - * @param \Magento\Framework\DataObject $customer + * @param DataObject $customer * @return $this */ - protected function _afterSave(\Magento\Framework\DataObject $customer) + protected function _afterSave(DataObject $customer) { $this->getNotificationStorage()->add( NotificationStorage::UPDATE_CUSTOMER_SESSION, @@ -250,9 +302,9 @@ protected function _afterSave(\Magento\Framework\DataObject $customer) /** * Retrieve select object for loading base entity row * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @param string|int $rowId - * @return \Magento\Framework\DB\Select + * @return Select */ protected function _getLoadRowSelect($object, $rowId) { @@ -270,7 +322,7 @@ protected function _getLoadRowSelect($object, $rowId) * @param \Magento\Customer\Model\Customer $customer * @param string $email * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email) { @@ -285,7 +337,7 @@ public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email) if ($customer->getSharingConfig()->isWebsiteScope()) { if (!$customer->hasData('website_id')) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("A customer website ID wasn't specified. The ID must be specified to use the website scope.") ); } @@ -390,10 +442,10 @@ public function getWebsiteId($customerId) /** * Custom setter of increment ID if its needed * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @return $this */ - public function setNewIncrementId(\Magento\Framework\DataObject $object) + public function setNewIncrementId(DataObject $object) { if ($this->_scopeConfig->getValue( \Magento\Customer\Model\Customer::XML_PATH_GENERATE_HUMAN_FRIENDLY_ID, @@ -419,7 +471,7 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c if (is_string($passwordLinkToken) && !empty($passwordLinkToken)) { $customer->setRpToken($passwordLinkToken); $customer->setRpTokenCreatedAt( - (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) + (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT) ); } return $this; @@ -469,7 +521,7 @@ public function updateSessionCutOff(int $customerId, int $timestamp): void /** * @inheritDoc */ - protected function _afterLoad(\Magento\Framework\DataObject $customer) + protected function _afterLoad(DataObject $customer) { if ($customer->getData('rp_token')) { $rpToken = $customer->getData('rp_token'); diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 1be4f684e9b9d..99720afc9829f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; @@ -407,8 +406,8 @@ public function getById($customerId) * Retrieve customers which match a specified criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface to determine - * which call to use to get detailed information about all attributes for an object. + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface + * to determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface @@ -540,6 +539,14 @@ private function prepareCustomerData(array $customerData): array { if (isset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES])) { foreach ($customerData[CustomerInterface::CUSTOM_ATTRIBUTES] as $attribute) { + if (empty($attribute['value']) + && !empty($attribute['selected_options']) + && is_array($attribute['selected_options']) + ) { + $attribute['value'] = implode(',', array_map(function ($option): string { + return $option['value'] ?? ''; + }, $attribute['selected_options'])); + } $customerData[$attribute['attribute_code']] = $attribute['value']; } unset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES]); diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index d0115dbee72bb..ec47afe8e065a 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -393,6 +393,19 @@ public function getCustomerGroupId() return Group::NOT_LOGGED_IN_ID; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_customer = null; + $this->_customerModel = null; + $this->setCustomerId(null); + $this->setCustomerGroupId($this->groupManagement->getNotLoggedInGroup()->getId()); + $this->_isCustomerIdChecked = null; + parent::_resetState(); + } + /** * Checking customer login status * diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index ec2d90c4a7db1..6e69681a845f7 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -212,6 +212,11 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = $gatewayResponse->setRequestMessage(__('Please enter a valid VAT number.')); } } catch (\Exception $exception) { + $this->logger->error( + sprintf('VAT Number validation failed with message: %s', $exception->getMessage()), + ['exception' => $exception] + ); + $gatewayResponse->setIsValid(false); $gatewayResponse->setRequestDate(''); $gatewayResponse->setRequestIdentifier(''); diff --git a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php index 165c411a46336..4b6630c0e7a34 100644 --- a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php +++ b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php @@ -6,21 +6,44 @@ namespace Magento\Customer\Observer\Visitor; +use Magento\Customer\Model\Visitor; use Magento\Framework\Event\Observer; +use Magento\Framework\Session\SessionManagerInterface; /** * Visitor Observer + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class InitByRequestObserver extends AbstractVisitorObserver { /** - * initByRequest + * @var SessionManagerInterface + */ + private $sessionManager; + + /** + * @param Visitor $visitor + * @param SessionManagerInterface $sessionManager + */ + public function __construct( + Visitor $visitor, + SessionManagerInterface $sessionManager + ) { + parent::__construct($visitor); + $this->sessionManager = $sessionManager; + } + + /** + * Init visitor by request * * @param Observer $observer * @return void */ public function execute(Observer $observer) { + if ($observer->getRequest()->getFullActionName() === 'customer_account_loginPost') { + $this->sessionManager->setVisitorData(['do_customer_login' => true]); + } $this->visitor->initByRequest($observer); } } diff --git a/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php new file mode 100644 index 0000000000000..5b5c8ce1fc0ca --- /dev/null +++ b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Plugin; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\AsynchronousOperations\Model\MassSchedule; + +/** + * Plugin to validate anonymous request for asynchronous operations containing group id. + */ +class AsyncRequestCustomerGroupAuthorization +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * Validate groupId for anonymous request + * + * @param MassSchedule $massSchedule + * @param string $topic + * @param array $entitiesArray + * @param string|null $groupId + * @param string|null $userId + * @return null + * @throws AuthorizationException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePublishMass( + MassSchedule $massSchedule, + string $topic, + array $entitiesArray, + string $groupId = null, + string $userId = null + ) { + foreach ($entitiesArray as $entityParams) { + foreach ($entityParams as $entity) { + if ($entity instanceof CustomerInterface) { + $groupId = $entity->getGroupId(); + if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { + $params = ['resources' => self::ADMIN_RESOURCE]; + throw new AuthorizationException( + __("The consumer isn't authorized to access %resources.", $params) + ); + } + } + } + } + return null; + } +} diff --git a/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php new file mode 100644 index 0000000000000..ad2d8ed1cf974 --- /dev/null +++ b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Plugin\Webapi\Controller\Rest; + +use Magento\Webapi\Controller\Rest\ParamsOverrider; + +/** + * Validates Customer Data + */ +class ValidateCustomerData +{ + private const CUSTOMER_KEY = 'customer'; + + /** + * Before Overriding to validate data + * + * @param ParamsOverrider $subject + * @param array $inputData + * @param array $parameters + * @return array[] + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeOverride(ParamsOverrider $subject, array $inputData, array $parameters): array + { + if (isset($inputData[self:: CUSTOMER_KEY])) { + $inputData[self:: CUSTOMER_KEY] = $this->validateInputData($inputData[self:: CUSTOMER_KEY]); + } + return [$inputData, $parameters]; + } + + /** + * Validates InputData + * + * @param array $inputData + * @return array + */ + private function validateInputData(array $inputData): array + { + $result = []; + + $data = array_filter($inputData, function ($k) use (&$result) { + $key = is_string($k) ? strtolower($k) : $k; + return !isset($result[$key]) && ($result[$key] = true); + }, ARRAY_FILTER_USE_KEY); + + return array_map(function ($value) { + return is_array($value) ? $this->validateInputData($value) : $value; + }, $data); + } +} diff --git a/app/code/Magento/Customer/README.md b/app/code/Magento/Customer/README.md index 4b2b1a4d6211d..f63b7f0633279 100644 --- a/app/code/Magento/Customer/README.md +++ b/app/code/Magento/Customer/README.md @@ -1,7 +1,7 @@ # Magento_Customer module -This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront. -For customer passwords, the module implements upgrading hashes. +This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront. +For customer passwords, the module implements upgrading hashes. ## Installation @@ -12,6 +12,7 @@ This module is dependent on the following modules: - `Magento_Directory` The following modules depend on this module: + - `Magento_Captcha` - `Magento_Catalog` - `Magento_CatalogCustomerGraphQl` @@ -31,6 +32,7 @@ The following modules depend on this module: - `Magento_WishlistGraphQl` The Magento_Customer module creates the following tables in the database: + - `customer_entity` - `customer_entity_datetime` - `customer_entity_decimal` @@ -65,17 +67,19 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve The module dispatches the following events: #### Block + - `adminhtml_block_html_before` event in the `\Magento\Customer\Block\Adminhtml\Edit\Tab\Carts::_toHtml` method. Parameters: - `block` is a `$this` object (`Magento\Customer\Block\Adminhtml\Edit\Tab\Carts` class) - + #### Controller + - `customer_register_success` event in the `\Magento\Customer\Controller\Account\CreatePost::execute` method. Parameters: - `account_controller` is a `$this` object (`\Magento\Customer\Controller\Account\CreatePost` class) - `customer` is a customer object (`\Magento\Customer\Model\Data\Customer` class) - + - `customer_account_edited` event in the `\Magento\Customer\Controller\Account\EditPost::dispatchSuccessEvent` method. Parameters: - `email` is a customer email (`string` type) - + - `adminhtml_customer_prepare_save` event in the `\Magento\Customer\Controller\Adminhtml\Index\Save::execute` method. Parameters: - `customer` is a customer object to be saved (`\Magento\Customer\Model\Data\Customer` class) - `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface. @@ -85,6 +89,7 @@ The module dispatches the following events: - `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface. #### Model + - `customer_customer_authenticated` event in the `\Magento\Customer\Model\AccountManagement::authenticate` method. Parameters: - `model` is a customer object (`\Magento\Customer\Model\Customer` class) - `password` is a customer password (`string` type) @@ -134,6 +139,7 @@ For information about an event in Magento 2, see [Events and observers](https:// ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `customer_address_edit` - `customer_group_index` @@ -146,7 +152,7 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `customer_index_viewcart` - `customer_index_viewwishlist` - `customer_online_index` - + - `view/frontend/layout`: - `customer_account` - `customer_account_confirmation` @@ -202,25 +208,25 @@ For more information about a layout in Magento 2, see the [Layout documentation] #### Metadata - `\Magento\Customer\Api\MetadataInterface`: - - retrieve all attributes filtered by form code + - retrieve all attributes filtered by form code - retrieve attribute metadata by attribute code - get all attribute metadata - get custom attributes metadata for the given data interface - + - `\Magento\Customer\Api\MetadataManagementInterface`: - check whether attribute is searchable in admin grid and it is allowed - check whether attribute is filterable in admin grid and it is allowed - + #### Customer address - `\Magento\Customer\Api\AddressMetadataInterface`: - retrieve information about customer address attributes metadata - extends `Magento\Customer\MetadataInterface` - + - `\Magento\Customer\Api\AddressMetadataManagementInterface`: - manage customer address attributes metadata - extends `Magento\Customer\Api\MetadataManagementInterface` - + - `\Magento\Customer\Api\AddressRepositoryInterface`: - save customer address - get customer address by address ID @@ -237,7 +243,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\Customer\Model\Address\CustomAttributeListInterface` - retrieve list of customer addresses custom attributes - + #### Customer - `\Magento\Customer\Api\AccountManagementInterface`: @@ -260,21 +266,21 @@ For more information about a layout in Magento 2, see the [Layout documentation] - retrieve default billing address for the given customer ID - retrieve default shipping address for the given customer ID - get hashed password - + - `\Magento\Customer\Api\CustomerManagementInterface`: - provide the number of customer count - + - `\Magento\Customer\Api\CustomerMetadataInterface`: - retrieve information about customer attributes metadata - extends `Magento\Customer\MetadataInterface` - + - `\Magento\Customer\Api\CustomerMetadataManagementInterface`: - manage customer attributes metadata - extends `Magento\Customer\Api\MetadataManagementInterface` - + - `\Magento\Customer\Api\CustomerNameGenerationInterface`: - concatenate all customer name parts into full customer name - + - `\Magento\Customer\Api\CustomerRepositoryInterface`: - create or update a customer - get customer by customer EMAIL @@ -294,19 +300,19 @@ For more information about a layout in Magento 2, see the [Layout documentation] - send email with new customer password - send email with reset password confirmation link - send email with new account related information - + #### Customer group - `\Magento\Customer\Api\CustomerGroupConfigInterface`: - set system default customer group - + - `\Magento\Customer\Api\GroupManagementInterface`: - check if customer group can be deleted - get default customer group - get customer group representing customers not logged in - get all customer groups except group representing customers not logged in - get customer group representing all customers - + - `\Magento\Customer\Api\GroupRepositoryInterface`: - save customer group - get customer group by group ID @@ -319,12 +325,13 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\Customer\Model\Customer\Source\GroupSourceLoggedInOnlyInterface` - get customer group attribute source - + For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ### UI components You can extend customer and customer address updates using the configuration files located in the `view/adminhtml/ui_component` and `view/base/ui_component` directories: + - `view/adminhtml/ui_component`: - `customer_address_form` - `customer_address_listing` @@ -334,12 +341,13 @@ You can extend customer and customer address updates using the configuration fil - `view/base/ui_component`: - `customer_form` - + For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information More information can get at articles: + - [Customer Configurations](https://docs.magento.com/user-guide/configuration/customers/customer-configuration.html) - [Customer Attributes](https://docs.magento.com/user-guide/stores/attributes-customer.html) - [Customer Address Attributes](https://docs.magento.com/user-guide/stores/attributes-customer-address.html) @@ -349,11 +357,13 @@ More information can get at articles: ### Console commands Magento_Customer provides console commands: + - `bin/magento customer:hash:upgrade` - upgrades a customer password hash to the latest hash algorithm ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `visitor_clean` - clean visitor's outdated records [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). @@ -361,6 +371,7 @@ Cron group configuration can be set at `etc/crontab.xml`: ### Indexers This module introduces the following indexers: + - `customer_grid` - customer grid indexer [Learn how to manage the indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html). diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php new file mode 100644 index 0000000000000..eae1c83e9f3c4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Fixture; + +use Magento\Customer\Model\Attribute; +use Magento\Customer\Model\ResourceModel\Attribute as ResourceModelAttribute; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\AttributeFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class CustomerAttribute implements RevertibleDataFixtureInterface +{ + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @var CustomerAttributeDefaultData + */ + private CustomerAttributeDefaultData $customerAttributeDefaultData; + + /** + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + * @param AttributeFactory $attributeFactory + * @param ResourceModelAttribute $resourceModelAttribute + * @param CustomerAttributeDefaultData $customerAttributeDefaultData + */ + public function __construct( + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository, + AttributeFactory $attributeFactory, + ResourceModelAttribute $resourceModelAttribute, + CustomerAttributeDefaultData $customerAttributeDefaultData + ) { + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeFactory = $attributeFactory; + $this->resourceModelAttribute = $resourceModelAttribute; + $this->attributeRepository = $attributeRepository; + $this->customerAttributeDefaultData = $customerAttributeDefaultData; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + $defaultData = $this->customerAttributeDefaultData->getData(); + if (empty($data['entity_type_id'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + /** @var Attribute $attr */ + $attr = $this->attributeFactory->createAttribute(Attribute::class, $defaultData); + $mergedData = $this->processor->process($this, $this->dataMerger->merge($defaultData, $data)); + $attr->setData($mergedData); + if (isset($data['website_id'])) { + $attr->setWebsite($data['website_id']); + } + $this->resourceModelAttribute->save($attr); + return $attr; + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php new file mode 100644 index 0000000000000..c95cdce2d20f1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Fixture; + +class CustomerAttributeDefaultData +{ + private const DEFAULT_DATA = [ + 'entity_type_id' => null, + 'attribute_id' => null, + 'attribute_code' => 'attribute%uniqid%', + 'default_frontend_label' => 'Attribute%uniqid%', + 'frontend_labels' => [], + 'frontend_input' => 'text', + 'backend_type' => 'varchar', + 'is_required' => false, + 'is_user_defined' => true, + 'note' => null, + 'backend_model' => null, + 'source_model' => null, + 'default_value' => null, + 'is_unique' => '0', + 'frontend_class' => null, + 'used_in_forms' => [], + 'sort_order' => 0, + 'attribute_set_id' => null, + 'attribute_group_id' => null, + 'input_filter' => null, + 'multiline_count' => 0, + 'validate_rules' => null, + 'website_id' => null, + 'is_visible' => 1, + 'scope_is_visible' => 1, + ]; + + /** + * @var array + */ + private $defaultData; + + /** + * @param array $defaultData + */ + public function __construct(array $defaultData = []) + { + $this->defaultData = array_merge(self::DEFAULT_DATA, $defaultData); + } + + /** + * Return default data + */ + public function getData(): array + { + return $this->defaultData; + } +} diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php index a9a96e42a5d2a..b3649b0546c47 100644 --- a/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php +++ b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php @@ -3,20 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Customer\Test\Fixture; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\Hydrator; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Tax\Api\TaxClassRepositoryInterface; +use Magento\TestFramework\Fixture\Api\DataMerger; use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; /** @@ -35,50 +32,46 @@ class CustomerGroup implements RevertibleDataFixtureInterface private ServiceFactory $serviceFactory; /** - * @var TaxClassRepositoryInterface + * @var Hydrator */ - private TaxClassRepositoryInterface $taxClassRepository; - - /** @var Hydrator */ private Hydrator $hydrator; + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $dataProcessor; + /** * @param ServiceFactory $serviceFactory - * @param TaxClassRepositoryInterface $taxClassRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param Hydrator $hydrator + * @param DataMerger $dataMerger + * @param ProcessorInterface $dataProcessor */ public function __construct( ServiceFactory $serviceFactory, - TaxClassRepositoryInterface $taxClassRepository, - Hydrator $hydrator + Hydrator $hydrator, + DataMerger $dataMerger, + ProcessorInterface $dataProcessor ) { $this->serviceFactory = $serviceFactory; - $this->taxClassRepository = $taxClassRepository; $this->hydrator = $hydrator; + $this->dataMerger = $dataMerger; + $this->dataProcessor = $dataProcessor; } /** - * {@inheritdoc} - * @param array $data Parameters. Same format as Customer::DEFAULT_DATA. - * @return DataObject|null - * @throws LocalizedException - * @throws NoSuchEntityException + * @inheritdoc */ public function apply(array $data = []): ?DataObject { - $customerGroupSaveService = $this->serviceFactory->create( - GroupRepositoryInterface::class, - 'save' - ); - $data = self::DEFAULT_DATA; - if (!empty($data['tax_class_id'])) { - $data[GroupInterface::TAX_CLASS_ID] = $this->taxClassRepository->get($data['tax_class_id'])->getClassId(); - } - - $customerGroup = $customerGroupSaveService->execute( + $customerGroup = $this->serviceFactory->create(GroupRepositoryInterface::class, 'save')->execute( [ - 'group' => $data, + 'group' => $this->dataProcessor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)) ] ); @@ -90,8 +83,7 @@ public function apply(array $data = []): ?DataObject */ public function revert(DataObject $data): void { - $service = $this->serviceFactory->create(GroupRepositoryInterface::class, 'deleteById'); - $service->execute( + $this->serviceFactory->create(GroupRepositoryInterface::class, 'deleteById')->execute( [ 'id' => $data->getId() ] diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml new file mode 100644 index 0000000000000..afa6b44f6ee61 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.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="AdminCustomerShowCompanyActionGroup"> + <annotations> + <description>Goes to the customer configuration. Set "Show Company" with provided value.</description> + </annotations> + <arguments> + <argument name="value" type="string" defaultValue="{{ShowCompany.optional}}"/> + </arguments> + <amOnPage url="{{AdminCustomerConfigPage.url('#customer_address-link')}}" stepKey="openCustomerConfigPage"/> + <waitForPageLoad stepKey="waitCustomerConfigPage"/> + <scrollTo selector="{{AdminCustomerConfigSection.showCompany}}" x="0" y="-100" stepKey="scrollToShowCompany"/> + <uncheckOption selector="{{AdminCustomerConfigSection.showCompanyInherit}}" stepKey="uncheckUseSystem"/> + <selectOption selector="{{AdminCustomerConfigSection.showCompany}}" userInput="{{value}}" stepKey="fillShowCompany"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml new file mode 100644 index 0000000000000..a71d0767835b4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.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="AdminFillAndSaveCustomerAddressWithoutRegionActionGroup" extends="AdminFillAndSaveCustomerAddressInformationActionGroup"> + <annotations> + <description>Fill and save customer address information omitting the region.</description> + </annotations> + <arguments> + <argument name="address" type="entity"/> + </arguments> + <remove keyForRemoval="fillRegion"/> + <selectOption selector="{{AdminCustomerAddressesSection.state}}" userInput="{{address.state}}" stepKey="fillRegion" after="clickRegionToOpenListOfRegions"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml new file mode 100644 index 0000000000000..49ef1ad9cede9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMarketingInviteeCustomerGroupActionGroup"> + <arguments> + <argument name="inviteeGroup" type="string" defaultValue="{{GeneralCustomerGroup.code}}"/> + </arguments> + + <selectOption selector="{{AdminCustomerAccountInformationSection.inviteeGroup}}" userInput="{{inviteeGroup}}" stepKey="selectInviteeGroup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml new file mode 100644 index 0000000000000..219bc9f4482ae --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.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="AdminRemoveRegionFromCustomerAddressInformationActionGroup" > + <annotations> + <description>Remove region from customer address information.</description> + </annotations> + <selectOption selector="{{AdminCustomerAddressesSection.state}}" userInput="Please select a region, state or province." stepKey="removeState"/> + <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAfterRemovingRegion"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml new file mode 100644 index 0000000000000..074b93d860f19 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml @@ -0,0 +1,18 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="EnterAddressDetailsActionGroup" extends="EnterCustomerAddressInfoActionGroup"> + <annotations> + <description>Removed specific page. Fills in the required details </description> + </annotations> + + <remove keyForRemoval="goToAddressPage"/> + <remove keyForRemoval="saveAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml new file mode 100644 index 0000000000000..d31bacf807ec3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.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="FillNewCustomerAddressFieldsActionGroup" extends="FillNewCustomerAddressRequiredFieldsActionGroup"> + <annotations> + <description>Select country before select state </description> + </annotations> + <arguments> + <argument name="address" type="entity"/> + </arguments> + + <remove keyForRemoval="selectCountry"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="{{address.country}}" stepKey="selectCountryField" after="fillStreetAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..567122c45317e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.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="StorefrontDeleteStoredPaymentMethodActionGroup"> + <annotations> + <description>Goes to the Stored Payment Method and delete the 2nd card</description> + </annotations> + <arguments> + <argument name="card" type="entity" defaultValue="StoredPaymentMethods"/> + </arguments> + + <click selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteBtn(card.cardExpire)}}" stepKey="clickOnDelete"/> + <waitForElementVisible selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteMessage}}" stepKey="waitForMessageToVisible"/> + <seeElement selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteMessage}}" stepKey="seeDeleteConfirmationMessage1"/> + <click selector="{{StorefrontCustomerStoredPaymentMethodsSection.delete}}" stepKey="clickOnDeleteInAlert"/> + <waitForPageLoad stepKey="waitForCustomersGridIsLoaded"/> + <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.successMessage}}" userInput="Stored Payment Method was successfully removed" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml new file mode 100644 index 0000000000000..f08a2422e0230 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.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="StorefrontFillCustomerCreateAnAccountActionGroup" extends="StorefrontFillCustomerAccountCreationFormActionGroup"> + <annotations> + <description>Fills in the provided Customer details on the Storefront Customer creation page.</description> + </annotations> + + <remove keyForRemoval="fillFirstName"/> + <remove keyForRemoval="fillLastName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml new file mode 100644 index 0000000000000..245ed2c8ca465 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.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="StorefrontRemoveRegionFromCustomerAddressFormActionGroup" > + <annotations> + <description>Remove region from customer address form.</description> + </annotations> + <selectOption selector="{{StorefrontCustomerAddressFormSection.state}}" userInput="Please select a region, state or province." stepKey="removeStateForStorefront"/> + </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 d47409cb0953d..c6eb85aacfb20 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -248,6 +248,24 @@ <data key="default_shipping">true</data> <data key="default_billing">true</data> </entity> + <entity name="addressNoCompany" type="address"> + <data key="company"/> + <data key="firstname">Fn</data> + <data key="lastname">Ln</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <data key="vat_id">47458714</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="updateCustomerUKAddress" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -478,4 +496,19 @@ <data key="default_billing">true</data> <data key="telephone">613-582-4782</data> </entity> + <entity name="Switzerland_Address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>Kapelle St.</item> + <item>Niklaus 3</item> + </array> + <data key="city">Baden</data> + <data key="country_id">CH</data> + <data key="country">Switzerland</data> + <data key="state">Aargau</data> + <data key="postcode">5555</data> + <data key="telephone">555-55-555-55</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml index 354ff72f62c48..47b37a4a8e264 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml @@ -18,6 +18,11 @@ <data key="optional">Optional</data> <data key="required">Required</data> </entity> + <entity name="ShowCompany"> + <data key="no">No</data> + <data key="optional">Optional</data> + <data key="required">Required</data> + </entity> <entity name="CustomerConfigurationSectionNameAndAddressOptions"> <data key="id">customer_address-head</data> <data key="title">Name and Address Options</data> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index 1752cb6d04c3d..fd1fc32996ae7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -20,6 +20,7 @@ <element name="email" type="input" selector="input[name='customer[email]']"/> <element name="disableAutomaticGroupChange" type="input" selector="input[name='customer[disable_auto_group_change]']"/> <element name="group" type="select" selector="[name='customer[group_id]']"/> + <element name="inviteeGroup" type="select" selector="div[data-index='group_id'] select[name='group_id']"/> <element name="groupIdValue" type="text" selector="//*[@name='customer[group_id]']/option"/> <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> <element name="associateToWebsite" type="select" selector="//select[@name='customer[website_id]']"/> @@ -38,5 +39,6 @@ <element name="customerAttribute" type="input" selector="//input[contains(@name,'{{attributeCode}}')]" parameterized="true"/> <element name="attributeImage" type="block" selector="//div[contains(concat(' ',normalize-space(@class),' '),' file-uploader-preview ')]//img"/> <element name="dateOfBirthValidationErrorField" type="text" selector="input[name='customer[dob]'] ~ label.admin__field-error"/> + <element name="customerAttributeNew" type="input" selector="(//input[contains(@name,'{{attributeCode}}')])[{{index}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml index 791ac991bb8c5..a8246586fd3cc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -15,5 +15,7 @@ <element name="showDateOfBirthInherit" type="select" selector="#customer_address_dob_show_inherit"/> <element name="showTelephone" type="select" selector="#customer_address_telephone_show"/> <element name="showTelephoneInherit" type="checkbox" selector="#customer_address_telephone_show_inherit"/> + <element name="showCompany" type="select" selector="#customer_address_company_show"/> + <element name="showCompanyInherit" type="select" selector="#customer_address_company_show_inherit"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml index 099c8da065525..2ad315fa840e8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml @@ -15,7 +15,7 @@ <element name="datedAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true"/> <element name="dropDownAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true"/> <element name="dropDownOptionAttribute" type="text" selector="//*[@id='{{var}}']/option[2]" parameterized="true"/> - <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[3]" parameterized="true"/> + <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[2]" parameterized="true"/> <element name="yesNoAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true"/> <element name="yesNoOptionAttribute" type="select" selector="//select[@id='{{var}}']/option[2]" parameterized="true"/> <element name="selectedOption" type="text" selector="//select[@id='{{var}}']/option[@selected='selected']" parameterized="true"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml index f6587a757ff3e..b1eeeec6de62e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml @@ -13,6 +13,6 @@ <element name="password" type="input" selector="#pass"/> <element name="signIn" type="button" selector="(//button[@id='send2'][contains(@class, 'login')])[1]" timeout="30"/> <element name="forgotYourPassword" type="button" selector="//a[@class='action']//span[contains(text(),'Forgot Your Password?')]" timeout="30"/> - <element name="createAnAccount" type="button" selector="//div[contains(@class,'actions-toolbar')]//a[contains(.,'Create an Account')]" timeout="30"/> + <element name="createAnAccount" type="button" selector="(//div[contains(@class,'actions-toolbar')]//a[contains(.,'Create an Account')])[last()]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml index d6b586e42f28c..ba8159948701d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml @@ -11,5 +11,9 @@ <section name="StorefrontCustomerStoredPaymentMethodsSection"> <element name="cardNumber" type="text" selector="td.card-number"/> <element name="expirationDate" type="text" selector="td.card-expire"/> + <element name="deleteBtn" type="button" selector=".//*[contains(text(),'{{var1}}')]/../td[@class='col actions']//button" parameterized="true"/> + <element name="delete" type="button" selector="(//*[@class='action primary']/span)[2]"/> + <element name="deleteMessage" type="text" selector="//div[@class='modal-content']//div[text()='Are you sure you want to delete this card: 0002?']"/> + <element name="successMessage" type="text" selector=".//*[@class='message-success success message']/div"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml index e213185f28f23..0fff3a7827545 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-94814"/> <group value="customer"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml index b7096625aca85..bd8d0807c3092 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-22025"/> <useCaseId value="MC-17259"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml index 205da22833cca..43459c4ecced2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml @@ -18,6 +18,7 @@ <stories value="Customer Edit"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml index 543f26a1aaf65..b4e21e7cba02f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml index 6c57fd4dfb4b8..b88faaa63904e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml index a1595cfebb9f3..912a9933af35c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <description value="When Admin tries to create a customer in second website with the global account sharing is enabled, then Admin should be able to do so."/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="CustomerAccountSharingGlobal" stepKey="setConfigCustomerAccountToGlobal"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml index 78adcd9058ec2..da4552fef31e4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5310"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 7bc01cab564cc..61d39579a011f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="create"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml index 631349cb61960..da2da9e21e05a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5311"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml index cffa1bc95ac6c..d939c6919224b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5309"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml index 29941d7223c08..d1ce96f77a256 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5313"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml index 7f66b657180f1..86182b230830d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5308"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml index 8f2e20e90d758..55bbe09b017a2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5307"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml index 2cd231d0bf396..e5784c1a72d04 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml index 6d917d3d18b43..b3c73b3d08b0e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml index c3dbad6708156..161cd55d8a79e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5312"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml index e8198cb79262e..04636d5614a5c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-5301"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml index 3416c64a7e9d7..640f068ad8e59 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-5303"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Tax Class "Customer tax class"--> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml new file mode 100644 index 0000000000000..6a8430df0cd88 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer attribute change from required to no"/> + <title value="Admin should be able to save customer after changing attributes from required to no"/> + <description value="Admin should be able to save customer after changing attributes from required to no in default scope"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-6748"/> + <group value="customer"/> + </annotations> + <before> + <!-- Login to admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Create a customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfiguration"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScope"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + + <!-- Set "Show Date of Birth" to Required and save in default config scope --> + <actionGroup ref="AdminCheckUseSystemValueActionGroup" stepKey="checkUseSystemValue"> + <argument name="rowId" value="row_customer_address_company_show"/> + </actionGroup> + <click selector="{{StoreConfigSection.Save}}" stepKey="saveConfig"/> + + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Logout from admin --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfiguration"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScope"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + <!-- Set "Show Company" to Required and save in default config scope --> + <actionGroup ref="AdminCustomerShowCompanyActionGroup" stepKey="setShowCompanyRequiredDefaultScope"> + <argument name="value" value="{{ShowCompany.required}}"/> + </actionGroup> + + <!-- Open the customer edit page --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="goToCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!-- Switch the addresses tab --> + <actionGroup ref="AdminOpenAddressesTabFromCustomerEditPageActionGroup" stepKey="openAddressesTab"/> + <!-- Click "Add New Address" --> + <actionGroup ref="AdminClickAddNewAddressButtonOnCustomerAddressesTabActionGroup" stepKey="clickAddNewAddressButton"/> + <!-- Fill address --> + <actionGroup ref="AdminFillAndSaveCustomerAddressInformationActionGroup" stepKey="fillAndSaveCustomerAddressInformationActionGroup"> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Assert that the address is successfully added --> + <actionGroup stepKey="saveAndContinue" ref="AdminCustomerSaveAndContinue"/> + + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfigurationAgain"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScopeAgain"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + <!-- Set "Show Company" to Required and save in default config scope --> + <actionGroup ref="AdminCheckUseSystemValueActionGroup" stepKey="checkCompanyUseSystemValue"> + <argument name="rowId" value="row_customer_address_company_show"/> + </actionGroup> + + <click selector="{{StoreConfigSection.Save}}" stepKey="saveConfig"/> + <!-- Open the customer edit page --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="goToCustomerEditPageAgain"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!-- Switch the addresses tab --> + <actionGroup ref="AdminOpenAddressesTabFromCustomerEditPageActionGroup" stepKey="openAddressesTabAgain"/> + <!-- Click "Add New Address" --> + <actionGroup ref="AdminClickAddNewAddressButtonOnCustomerAddressesTabActionGroup" stepKey="clickAddNewAddressButtonAgain"/> + <!-- Fill address --> + <actionGroup ref="AdminFillAndSaveCustomerAddressInformationActionGroup" stepKey="fillAndSaveCustomerAddressInformationActionGroupAgain"> + <argument name="address" value="addressNoCompany"/> + </actionGroup> + <!-- Assert that the address is successfully added --> + <actionGroup stepKey="saveAndContinueAgain" ref="AdminCustomerSaveAndContinue"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml index d54977b2e2ab1..275fb21b70083 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-22173"/> <severity value="MAJOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="CustomerAccountSharingGlobal" stepKey="setConfigCustomerAccountToGlobal"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml index 207430c7bc7b9..8d3c6c50d055e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml index bc0c3e00d75aa..952dc38fca84b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14115"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml index eb10a9bf469c7..080b933cdcfdd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml index 8d5535a48f8a3..7303d2b083b27 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14114"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml index fe4a3ea39313b..fc8cc30f609cf 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml @@ -17,6 +17,7 @@ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml index 5ba49cbcefba4..9e263d88b2b8f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MAGETWO-94951"/> <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml index 059216036280a..af9a9db5809e7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-14587"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml index 5683a75f2a382..d3830c0f1f048 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MAGETWO-94816"/> <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml index 7929f91e778f7..12d3dda949ccc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml @@ -15,6 +15,7 @@ <description value="Edit customer if there is associated newsletter queue new"/> <severity value="BLOCKER"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml index f8f3dfe19d6e2..2536fce2b9885 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94815"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml index 7f1b1dfee7ce0..508d64cf18e9b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-37659"/> <severity value="CRITICAL"/> <group value="uI"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml index ef4dc560d4fee..9655303856a4e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-37660"/> <severity value="CRITICAL"/> <group value="uI"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml index 5f20eb9cd5e67..21b21ad772c9c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://github.com/magento/magento2/pull/24845"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -30,7 +31,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createSimpleCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml index 28ddac690a5e5..ef01fad2790dd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml @@ -15,6 +15,7 @@ <description value="Place an order when country allowed only on current website scope"/> <severity value="MAJOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -33,7 +34,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml index 4c4175bb32198..f1a98d8705c0e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml @@ -16,6 +16,7 @@ <description value="Back button on product page is redirecting to customer page if opened form shopping cart"/> <severity value="MINOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <!-- Create new product--> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml index e1cd7146856de..beb140129b27e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-30875"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml index 4d833cce920ec..df9ba8cb02ac8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94952"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml index a273d9e7431d9..f84aed64fdd49 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94953"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml index 3fa29aef9908e..fc6da1df7b30e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13623"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml index c6e370fb6a76b..847e144286c19 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13622"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> <remove keyForRemoval="goToCustomersGridPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml index d81d7da6b5b07..9be754b34e109 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13621"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> <remove keyForRemoval="goToCustomersGridPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml index 09ff169b1fac8..701d2fe0d00fe 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13619"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml index 7a68f48d2ab93..1a1185a083f38 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-5314"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml index c990c9ff659af..12817b17563a7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5315"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml index 04bdc4e6a608c..01cb4b16c8bc4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-99461"/> <useCaseId value="MAGETWO-99302"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="firstCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml index c4bcc4e3854d9..c533e352f1110 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-39783"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml index ff60ac92853c5..977d00696d3d2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml index 000db5d79f76a..1a4edd95c4a4d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-6441"/> <useCaseId value="MAGETWO-91523"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml index e32ae04495fe5..4d9cbb2a1837c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 6e7fe4e259d7a..cee34fb258aa3 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -23,6 +23,7 @@ </skip> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> <actionGroup ref="AdminLoginActionGroup" after="resetCookieForCart" stepKey="loginAsAdmin"/> </before> @@ -32,6 +33,7 @@ <actionGroup ref="DeleteCustomerFromAdminActionGroup" stepKey="deleteCustomerFromAdmin"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </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/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml index c98d20a32ba16..cdce67f5f9441 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-40713"/> <severity value="MAJOR"/> <group value="customers"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml index dd982077ccb69..a5bc3f744214d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-41141"/> <severity value="MAJOR"/> <group value="customers"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml index d4f851ee21c25..4b2d51aaf8cde 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-97364"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml index cec7f8460de5a..936ad2bc015ba 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-97500"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml index c3c8bd5d7c40e..82a72506b896e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-97364"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml index d9349dae29329..c90934d256e7f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MAGETWO-93289"/> <stories value="MAGETWO-66666: Adding a product to cart from category page with an expired session does not allow product to be added"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml index fe7a54bb23554..72883af621192 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml @@ -16,6 +16,7 @@ <description value="Check Show Password Functionality in Customer Password Update Form"/> <severity value="MAJOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml index a71d4944617ae..a225c89255831 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95028"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Log In--> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml index 5834772a41fab..2da490002fc29 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Customer"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml index 0d64ceb545831..f0f47e416ea28 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-23546"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml index d9e665ec7a2ad..ad1b6d0fb74ed 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-32413"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml index ef610831a721d..490b85c4176d5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-38532"/> <useCaseId value="MC-38509"/> <group value="customer"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml index 07ac295e5cce0..8703116303fe4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml index 547e0c648f43d..1a9930b2b0ef7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-34953"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> @@ -106,7 +107,7 @@ </before> <after> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="Product" stepKey="deleteProduct"/> <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml index 4309076df2146..b4e1d8134199f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27518"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml index faf03ad666bd1..be0d6736470bc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml @@ -16,6 +16,7 @@ <description value="Customer should be automatically redirected to account dashboard after login"/> <severity value="MINOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml index 2b0da367a9ec5..366d1805705b9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml @@ -16,6 +16,7 @@ <group value="module-customer"/> <severity value="MAJOR"/> <testCaseId value="MC-27411"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml index 62bab5669307b..95981aa11418c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml @@ -15,6 +15,7 @@ <title value="StoreFront Customer Newsletter Subscription"/> <description value="Customer can be subscribed to Newsletter Subscription on StoreFront"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml index 51efd4e23f5d3..479f4312a9d6b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5713"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml index c69c4dd071e38..d7b62c65ef97d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml index 7d7218c59d149..833386fa71162 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml @@ -16,6 +16,7 @@ <description value="Check duplicate Validate Message on Customer Login Form"/> <severity value="MAJOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml index a7dc3c7fde7f4..f72cba7b9f336 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-10913"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml index 7845d3cee44ef..5cd261c6ef2d1 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-72103"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml index 1f92b429603e6..723f8b62b9980 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml index d41b1cf86da59..2f1b41fb9f13b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-97502"/> <group value="customer"/> <group value="update"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml index 0539b50dcaac4..5265349acd030 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-97504"/> <group value="customer"/> <group value="update"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml index 7b5ad9d70fd7c..78ecb05eceebc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml index 9e5be5abe95a3..e0685bb46fe7f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10918"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml index 1f2c07c325c15..f4612b58253e5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10917"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml index c977334c5f857..762ee9ef49e73 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml @@ -18,6 +18,7 @@ <group value="Customer"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml new file mode 100644 index 0000000000000..f4b8198c097ce --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyCustomerAddressRegionFieldTest"> + <annotations> + <features value="Customer"/> + <stories value="The State-Region field should stay blank after it's cleared from the customer address and saved"/> + <title value="The State-Region field should stay blank after it's cleared from the customer address and saved"/> + <description value="When removing the state from the customer address details in the admin, the field must stay blank after save."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8302"/> + <useCaseId value="ACP2E-1609"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Open customer grid page and Navigate to customer edit page addresses tab for created customer--> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminNavigateCustomerEditPageAddressesTabActionGroup" stepKey="openEditCustomerPageWithAddresses"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!--Click on edit default billing address and update the address--> + <actionGroup ref="AdminClickEditLinkForDefaultBillingAddressActionGroup" stepKey="clickEditDefaultBillingAddress"/> + <actionGroup ref="AdminFillAndSaveCustomerAddressWithoutRegionActionGroup" stepKey="fillAndSaveCustomerAddressInformation"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + + <!--Verify state name in address details section--> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{updateCustomerFranceAddress.state}}" stepKey="seeStateInAddress"/> + + <!--Click on edit link for default billing address , remove the region--> + <actionGroup ref="AdminClickEditLinkForDefaultBillingAddressActionGroup" stepKey="clickEditDefaultBillingAddressAgain"/> + <actionGroup ref="AdminRemoveRegionFromCustomerAddressInformationActionGroup" stepKey="removeState"/> + + <!--Verify state name not visible under address details section--> + <dontSee userInput="{{updateCustomerFranceAddress.state}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="dontSeeStateInAddress"/> + + <!--Log in to Storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCreateCustomer"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <!--Go to customer address book and click edit default shipping address for storefront--> + <actionGroup ref="StorefrontGoToCustomerAddressesPageActionGroup" stepKey="goToCustomerAddressBook"/> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditDefaultShippingAddressForStorefront"/> + + <!--Update the address--> + <actionGroup ref="FillNewCustomerAddressRequiredFieldsActionGroup" stepKey="fillAddressForm"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + + <!--Verify state name in address details section--> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{updateCustomerFranceAddress.state}}" stepKey="seeAssertCustomerDefaultShippingAddressState"/> + + <!--Click on edit link for default shipping address , remove the region and click on save button--> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditDefaultShippingAddressForStorefrontAgain"/> + <actionGroup ref="StorefrontRemoveRegionFromCustomerAddressFormActionGroup" stepKey="fillAddressFormWithoutRegion"/> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddressAfterRemovingRegion"/> + <waitForPageLoad stepKey="waitForPageToBeSavedAddressAfterRemovingRegion"/> + + <!--Verify state name not visible under address details section--> + <dontSee userInput="{{updateCustomerFranceAddress.state}}" selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="dontSeeAssertCustomerDefaultShippingAddressState"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php index 9799470472534..1915f17238490 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php @@ -21,6 +21,7 @@ use Magento\Framework\Data\Form\Element\Select; use Magento\Framework\Data\FormFactory; use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; use Magento\Newsletter\Model\Subscriber; @@ -50,8 +51,6 @@ class NewsletterTest extends TestCase private $contextMock; /** - * Store manager - * * @var StoreManagerInterface|MockObject */ private $storeManager; @@ -101,12 +100,20 @@ class NewsletterTest extends TestCase */ private $shareConfig; + /** @var TimezoneInterface|MockObject */ + protected $localeDateMock; + /** * @inheritdoc */ protected function setUp(): void { $this->contextMock = $this->createMock(Context::class); + $this->localeDateMock = $this->getMockBuilder(TimezoneInterface::class) + ->disableOriginalConstructor() + ->setMethods(['formatDateTime']) + ->getMockForAbstractClass(); + $this->contextMock->expects($this->any())->method('getLocaleDate')->willReturn($this->localeDateMock); $this->registryMock = $this->createMock(Registry::class); $this->formFactoryMock = $this->createMock(FormFactory::class); $this->subscriberFactoryMock = $this->createPartialMock( @@ -161,6 +168,56 @@ public function testInitFormCanNotShowTab() $this->assertSame($this->model, $this->model->initForm()); } + /** + * Test getSubscriberStatusChangedDate + * + * @dataProvider getChangeStatusAtDataProvider + */ + public function testGetSubscriberStatusChangedDate($statusDate, $dateExpected) + { + $customerId = 999; + $websiteId = 1; + $storeId = 1; + $isSubscribed = true; + + $this->registryMock->method('registry')->with(RegistryConstants::CURRENT_CUSTOMER_ID) + ->willReturn($customerId); + + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $customer->method('getWebsiteId')->willReturn($websiteId); + $customer->method('getStoreId')->willReturn($storeId); + $customer->method('getId')->willReturn($customerId); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + + $subscriberMock = $this->getMockBuilder(Subscriber::class) + ->disableOriginalConstructor() + ->setMethods(['loadByCustomer', 'getChangeStatusAt', 'isSubscribed', 'getData']) + ->getMock(); + $statusDate = new \DateTime($statusDate); + $this->localeDateMock->method('formatDateTime')->with($statusDate)->willReturn($dateExpected); + + $subscriberMock->method('loadByCustomer')->with($customerId, $websiteId)->willReturnSelf(); + $subscriberMock->method('getChangeStatusAt')->willReturn($statusDate); + $subscriberMock->method('isSubscribed')->willReturn($isSubscribed); + $subscriberMock->method('getData')->willReturn([]); + $this->subscriberFactoryMock->expects($this->any())->method('create')->willReturn($subscriberMock); + $this->assertEquals($dateExpected, $this->model->getStatusChangedDate()); + } + + /** + * Data provider for testGetSubscriberStatusChangedDate + * + * @return array + */ + public function getChangeStatusAtDataProvider() + { + return + [ + ['',''], + ['Nov 22, 2023, 1:00:00 AM','Nov 23, 2023, 2:00:00 AM'] + ]; + } + /** * Test to initialize the form */ diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index e0e872d8fd13b..f30fd7facebbe 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -12,6 +12,8 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Controller\Account\Confirm; use Magento\Customer\Helper\Address; +use Magento\Customer\Model\Logger as CustomerLogger; +use Magento\Customer\Model\Log; use Magento\Customer\Model\Session; use Magento\Customer\Model\Url; use Magento\Framework\App\Action\Context; @@ -123,6 +125,16 @@ class ConfirmTest extends TestCase */ protected $redirectResultMock; + /** + * @var CustomerLogger|MockObject + */ + private $customerLoggerMock; + + /** + * @var Log|MockObject + */ + private $logMock; + /** * @inheritdoc */ @@ -143,6 +155,9 @@ protected function setUp(): void ->method('create') ->willReturn($this->urlMock); + $this->customerLoggerMock = $this->createMock(CustomerLogger::class); + $this->logMock = $this->createMock(Log::class); + $this->customerAccountManagementMock = $this->getMockForAbstractClass(AccountManagementInterface::class); $this->customerDataMock = $this->getMockForAbstractClass(CustomerInterface::class); @@ -195,7 +210,9 @@ protected function setUp(): void 'customerAccountManagement' => $this->customerAccountManagementMock, 'customerRepository' => $this->customerRepositoryMock, 'addressHelper' => $this->addressHelperMock, - 'urlFactory' => $urlFactoryMock + 'urlFactory' => $urlFactoryMock, + 'customerLogger' => $this->customerLoggerMock, + 'cookieMetadataManager' => $objectManagerHelper->getObject(PhpCookieManager::class), ] ); } @@ -218,6 +235,8 @@ public function testIsLoggedIn(): void } /** + * @param $customerId + * @param $key * @return void * @dataProvider getParametersDataProvider */ @@ -271,7 +290,8 @@ public function getParametersDataProvider(): array * @param $key * @param $vatValidationEnabled * @param $addressType - * @param Phrase $successMessage + * @param $lastLoginAt + * @param $successMessage * * @return void * @dataProvider getSuccessMessageDataProvider @@ -282,7 +302,8 @@ public function testSuccessMessage( $key, $vatValidationEnabled, $addressType, - Phrase $successMessage + $lastLoginAt, + $successMessage ): void { $this->customerSessionMock->expects($this->once()) ->method('isLoggedIn') @@ -292,7 +313,7 @@ public function testSuccessMessage( ->method('getParam') ->willReturnMap( [ - ['id', false, $customerId], + ['id', 0, $customerId], ['key', false, $key] ] ); @@ -333,6 +354,14 @@ public function testSuccessMessage( ['*/*/admin', ['_secure' => true], 'http://store.web/back'] ]); + $this->logMock->expects($vatValidationEnabled ? $this->never() : $this->once()) + ->method('getLastLoginAt') + ->willReturn($lastLoginAt); + $this->customerLoggerMock->expects($vatValidationEnabled ? $this->never() : $this->once()) + ->method('get') + ->with(1) + ->willReturn($this->logMock); + $this->addressHelperMock->expects($this->once()) ->method('isVatValidationEnabled') ->willReturn($vatValidationEnabled); @@ -356,12 +385,14 @@ public function testSuccessMessage( public function getSuccessMessageDataProvider(): array { return [ - [1, 1, false, null, __('Thank you for registering with %1.', 'frontend')], + [1, 1, false, null, 'some-datetime', null], + [1, 1, false, null, null, __('Thank you for registering with %1.', 'frontend')], [ 1, 1, true, Address::TYPE_BILLING, + null, __( 'If you are a registered VAT customer, please click <a href="%1">here</a>' . ' to enter your billing address for proper VAT calculation.', @@ -373,12 +404,13 @@ public function getSuccessMessageDataProvider(): array 1, true, Address::TYPE_SHIPPING, + null, __( 'If you are a registered VAT customer, please click <a href="%1">here</a>' . ' to enter your shipping address for proper VAT calculation.', 'http://store.web/customer/address/edit' ) - ] + ], ]; } @@ -389,7 +421,8 @@ public function getSuccessMessageDataProvider(): array * @param $successUrl * @param $resultUrl * @param $isSetFlag - * @param Phrase $successMessage + * @param $successMessage + * @param $lastLoginAt * * @return void * @dataProvider getSuccessRedirectDataProvider @@ -401,7 +434,8 @@ public function testSuccessRedirect( $successUrl, $resultUrl, $isSetFlag, - Phrase $successMessage + $lastLoginAt, + $successMessage ): void { $this->customerSessionMock->expects($this->once()) ->method('isLoggedIn') @@ -411,7 +445,7 @@ public function testSuccessRedirect( ->method('getParam') ->willReturnMap( [ - ['id', false, $customerId], + ['id', 0, $customerId], ['key', false, $key], ['back_url', false, $backUrl] ] @@ -437,23 +471,28 @@ public function testSuccessRedirect( ->with($this->customerDataMock) ->willReturnSelf(); - $this->messageManagerMock - ->method('addSuccess') + $this->messageManagerMock->method('addSuccess') ->with($successMessage) ->willReturnSelf(); - $this->messageManagerMock - ->expects($this->never()) + $this->messageManagerMock->expects($this->never()) ->method('addException'); - $this->urlMock - ->method('getUrl') + $this->urlMock->method('getUrl') ->willReturnMap([ ['customer/address/edit', null, 'http://store.web/customer/address/edit'], ['*/*/admin', ['_secure' => true], 'http://store.web/back'], ['*/*/index', ['_secure' => true], $successUrl] ]); + $this->logMock->expects($this->once()) + ->method('getLastLoginAt') + ->willReturn($lastLoginAt); + $this->customerLoggerMock->expects($this->once()) + ->method('get') + ->with(1) + ->willReturn($this->logMock); + $this->storeMock->expects($this->any()) ->method('getFrontendName') ->willReturn('frontend'); @@ -468,10 +507,7 @@ public function testSuccessRedirect( $this->scopeConfigMock->expects($this->any()) ->method('isSetFlag') - ->with( - Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD, - ScopeInterface::SCOPE_STORE - ) + ->with(Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD, ScopeInterface::SCOPE_STORE) ->willReturn($isSetFlag); $this->model->execute(); @@ -490,6 +526,7 @@ public function getSuccessRedirectDataProvider(): array null, 'http://example.com/back', true, + null, __('Thank you for registering with %1.', 'frontend'), ], [ @@ -499,6 +536,7 @@ public function getSuccessRedirectDataProvider(): array 'http://example.com/success', 'http://example.com/success', true, + null, __('Thank you for registering with %1.', 'frontend'), ], [ @@ -508,7 +546,18 @@ public function getSuccessRedirectDataProvider(): array 'http://example.com/success', 'http://example.com/success', false, + null, __('Thank you for registering with %1.', 'frontend'), + ], + [ + 1, + 1, + null, + 'http://example.com/success', + 'http://example.com/success', + false, + 'some data', + null, ] ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php new file mode 100644 index 0000000000000..074d40021a184 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php @@ -0,0 +1,421 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Helper\View; +use Magento\Customer\Model\AccountConfirmation; +use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\AccountManagementApi; +use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Data\CustomerSecure; +use Magento\Customer\Model\Metadata\Validator; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Authorization; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Math\Random; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Registry; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Stdlib\StringUtils; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test for validating anonymous request for synchronous operations containing group id. + * + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AccountManagementApiTest extends TestCase +{ + /** + * @var AccountManagement + */ + private $accountManagementMock; + + /** + * @var AccountManagementApi + */ + private $accountManagement; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var CustomerFactory|MockObject + */ + private $customerFactory; + + /** + * @var ManagerInterface|MockObject + */ + private $manager; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Random|MockObject + */ + private $random; + + /** + * @var Validator|MockObject + */ + private $validator; + + /** + * @var ValidationResultsInterfaceFactory|MockObject + */ + private $validationResultsInterfaceFactory; + + /** + * @var AddressRepositoryInterface|MockObject + */ + private $addressRepository; + + /** + * @var CustomerMetadataInterface|MockObject + */ + private $customerMetadata; + + /** + * @var CustomerRegistry|MockObject + */ + private $customerRegistry; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var EncryptorInterface|MockObject + */ + private $encryptor; + + /** + * @var Share|MockObject + */ + private $share; + + /** + * @var StringUtils|MockObject + */ + private $string; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepository; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @var TransportBuilder|MockObject + */ + private $transportBuilder; + + /** + * @var DataObjectProcessor|MockObject + */ + private $dataObjectProcessor; + + /** + * @var Registry|MockObject + */ + private $registry; + + /** + * @var View|MockObject + */ + private $customerViewHelper; + + /** + * @var \Magento\Framework\Stdlib\DateTime|MockObject + */ + private $dateTime; + + /** + * @var \Magento\Customer\Model\Customer|MockObject + */ + private $customer; + + /** + * @var DataObjectFactory|MockObject + */ + private $objectFactory; + + /** + * @var ExtensibleDataObjectConverter|MockObject + */ + private $extensibleDataObjectConverter; + + /** + * @var DateTimeFactory|MockObject + */ + private $dateTimeFactory; + + /** + * @var AccountConfirmation|MockObject + */ + private $accountConfirmation; + + /** + * @var MockObject|SessionManagerInterface + */ + private $sessionManager; + + /** + * @var MockObject|CollectionFactory + */ + private $visitorCollectionFactory; + + /** + * @var MockObject|SaveHandlerInterface + */ + private $saveHandler; + + /** + * @var MockObject|AddressRegistry + */ + private $addressRegistryMock; + + /** + * @var MockObject|SearchCriteriaBuilder + */ + private $searchCriteriaBuilderMock; + + /** + * @var AllowedCountries|MockObject + */ + private $allowedCountriesReader; + + /** + * @var Authorization|MockObject + */ + private $authorizationMock; + + /** + * @var CustomerSecure|MockObject + */ + private $customerSecure; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function setUp(): void + { + $this->customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']); + $this->manager = $this->getMockForAbstractClass(ManagerInterface::class); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->random = $this->createMock(Random::class); + $this->validator = $this->createMock(Validator::class); + $this->validationResultsInterfaceFactory = $this->createMock( + ValidationResultsInterfaceFactory::class + ); + $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); + $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class); + $this->customerRegistry = $this->createMock(CustomerRegistry::class); + + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); + $this->share = $this->createMock(Share::class); + $this->string = $this->createMock(StringUtils::class); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->transportBuilder = $this->createMock(TransportBuilder::class); + $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class); + $this->registry = $this->createMock(Registry::class); + $this->customerViewHelper = $this->createMock(View::class); + $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); + $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); + $this->objectFactory = $this->createMock(DataObjectFactory::class); + $this->addressRegistryMock = $this->createMock(AddressRegistry::class); + $this->extensibleDataObjectConverter = $this->createMock( + ExtensibleDataObjectConverter::class + ); + $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); + $this->customerSecure = $this->getMockBuilder(CustomerSecure::class) + ->onlyMethods(['addData', 'setData']) + ->addMethods(['setRpToken', 'setRpTokenCreatedAt']) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); + $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + + $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->authorizationMock = $this->createMock(Authorization::class); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->accountManagement = $this->objectManagerHelper->getObject( + AccountManagementApi::class, + [ + 'customerFactory' => $this->customerFactory, + 'eventManager' => $this->manager, + 'storeManager' => $this->storeManager, + 'mathRandom' => $this->random, + 'validator' => $this->validator, + 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, + 'addressRepository' => $this->addressRepository, + 'customerMetadataService' => $this->customerMetadata, + 'customerRegistry' => $this->customerRegistry, + 'logger' => $this->logger, + 'encryptor' => $this->encryptor, + 'configShare' => $this->share, + 'stringHelper' => $this->string, + 'customerRepository' => $this->customerRepository, + 'scopeConfig' => $this->scopeConfig, + 'transportBuilder' => $this->transportBuilder, + 'dataProcessor' => $this->dataObjectProcessor, + 'registry' => $this->registry, + 'customerViewHelper' => $this->customerViewHelper, + 'dateTime' => $this->dateTime, + 'customerModel' => $this->customer, + 'objectFactory' => $this->objectFactory, + 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + 'accountConfirmation' => $this->accountConfirmation, + 'sessionManager' => $this->sessionManager, + 'saveHandler' => $this->saveHandler, + 'visitorCollectionFactory' => $this->visitorCollectionFactory, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'addressRegistry' => $this->addressRegistryMock, + 'allowedCountriesReader' => $this->allowedCountriesReader, + 'authorization' => $this->authorizationMock + ] + ); + $this->accountManagementMock = $this->createMock(AccountManagement::class); + + $this->storeMock = $this->getMockBuilder( + StoreInterface::class + )->disableOriginalConstructor() + ->getMock(); + } + + /** + * Verify that only authorized request will be able to change groupId + * + * @param int $groupId + * @param int $customerId + * @param bool $isAllowed + * @param int $willThrowException + * @return void + * @throws AuthorizationException + * @throws LocalizedException + * @dataProvider customerDataProvider + */ + public function testBeforeCreateAccount( + int $groupId, + int $customerId, + bool $isAllowed, + int $willThrowException + ): void { + if ($willThrowException) { + $this->expectException(AuthorizationException::class); + } else { + $this->expectNotToPerformAssertions(); + } + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with('Magento_Customer::manage') + ->willReturn($isAllowed); + + $customer = $this->getMockBuilder(CustomerInterface::class) + ->addMethods(['setData']) + ->getMockForAbstractClass(); + $customer->method('getGroupId')->willReturn($groupId); + $customer->method('getId')->willReturn($customerId); + $customer->method('getWebsiteId')->willReturn(2); + $customer->method('getStoreId')->willReturn(1); + $customer->method('setData')->willReturn(1); + + $this->customerRepository->method('get')->willReturn($customer); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + $this->customerRepository->method('save')->willReturn($customer); + + if (!$willThrowException) { + $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer); + $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1); + $this->random->method('getUniqueHash')->willReturn('testabc'); + $date = $this->getMockBuilder(\DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeFactory->expects(static::once()) + ->method('create') + ->willReturn($date); + $date->expects(static::once()) + ->method('format') + ->with('Y-m-d H:i:s') + ->willReturn('2015-01-01 00:00:00'); + $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure); + $this->storeManager->method('getStores') + ->willReturn([$this->storeMock]); + } + $this->accountManagement->createAccount($customer); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + [3, 1, false, 1], + [3, 1, true, 0] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 9e68d53fd5949..e2b507f6fe37d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void $minPasswordLength = 5; $minCharacterSetsNum = 2; $defaultGroupId = 1; - $requestedGroupId = 3; $datetime = $this->prepareDateTimeFactory(); @@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void return null; } })); - $customer->expects($this->atLeastOnce()) - ->method('getGroupId') - ->willReturn($requestedGroupId); $customer ->method('setGroupId') ->willReturnOnConsecutiveCalls(null, $defaultGroupId); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php index cee6a8aefd1a4..29b95cdf79cfe 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php @@ -407,6 +407,7 @@ public function getStreetFullDataProvider() ["first line\nsecond line", ['first line', 'second line']], ['single line', ['single line']], ['single line', 'single line'], + ['single line', ['single line', null]], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php b/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php deleted file mode 100644 index fd06dbf6b8004..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Customer\Test\Unit\Model\App\FrontController; - -use Magento\Customer\Model\App\FrontController\DeleteCookieWhenCustomerNotExistPlugin; -use Magento\Framework\App\Response\Http as ResponseHttp; -use Magento\Customer\Model\Session; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Tests \Magento\Customer\Model\App\FrontController\DeleteCookieWhenCustomerNotExistPluginTest. - */ -class DeleteCookieWhenCustomerNotExistPluginTest extends TestCase -{ - /** - * @var DeleteCookieWhenCustomerNotExistPlugin - */ - protected DeleteCookieWhenCustomerNotExistPlugin $plugin; - - /** - * @var ResponseHttp|MockObject - */ - protected ResponseHttp|MockObject $responseHttpMock; - - /** - * @var Session|MockObject - */ - protected MockObject|Session $customerSessionMock; - - /** - * Set up - */ - protected function setUp(): void - { - $this->customerSessionMock = $this->createMock(Session::class); - $this->responseHttpMock = $this->createMock(ResponseHttp::class); - $this->plugin = new DeleteCookieWhenCustomerNotExistPlugin( - $this->responseHttpMock, - $this->customerSessionMock - ); - } - - public function testBeforeDispatch() - { - $this->customerSessionMock->expects($this->once()) - ->method('getCustomerId') - ->willReturn(0); - $this->plugin->beforeDispatch(); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php index 35f9b0b8371c3..c7ae84b1fa0c2 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php @@ -20,7 +20,13 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Magento\Framework\Session\StorageInterface; +/** + * Unit test for CustomerNotification plugin + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CustomerNotificationTest extends TestCase { private const STUB_CUSTOMER_ID = 1; @@ -65,6 +71,11 @@ class CustomerNotificationTest extends TestCase */ private $plugin; + /** + * @var StorageInterface|MockObject + */ + private $storage; + protected function setUp(): void { $this->sessionMock = $this->createMock(Session::class); @@ -87,19 +98,27 @@ protected function setUp(): void ->with(NotificationStorage::UPDATE_CUSTOMER_SESSION, self::STUB_CUSTOMER_ID) ->willReturn(true); + $this->storage = $this + ->getMockBuilder(StorageInterface::class) + ->addMethods(['getData', 'setData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->plugin = new CustomerNotification( $this->sessionMock, $this->notificationStorageMock, $this->appStateMock, $this->customerRepositoryMock, $this->loggerMock, - $this->requestMock + $this->requestMock, + $this->storage ); } public function testBeforeExecute() { $customerGroupId = 1; + $testSessionId = [uniqid()]; $customerMock = $this->getMockForAbstractClass(CustomerInterface::class); $customerMock->method('getGroupId')->willReturn($customerGroupId); @@ -116,6 +135,10 @@ public function testBeforeExecute() $this->sessionMock->expects($this->once())->method('setCustomerData')->with($customerMock); $this->sessionMock->expects($this->once())->method('setCustomerGroupId')->with($customerGroupId); $this->sessionMock->expects($this->once())->method('regenerateId'); + $this->storage->expects($this->once())->method('getData')->willReturn($testSessionId); + $this->storage + ->expects($this->once()) + ->method('setData'); $this->plugin->beforeExecute($this->actionMock); } diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php new file mode 100644 index 0000000000000..107df2c2863ef --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Plugin; + +use Magento\AsynchronousOperations\Model\MassSchedule; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Authorization; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Customer\Plugin\AsyncRequestCustomerGroupAuthorization; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for validating anonymous request for asynchronous operations containing group id. + */ +class AsyncRequestCustomerGroupAuthorizationTest extends TestCase +{ + /** + * @var Authorization|MockObject + */ + private $authorizationMock; + + /** + * @var AsyncRequestCustomerGroupAuthorization + */ + private $plugin; + + /** + * @var MassSchedule|MockObject + */ + private $massScheduleMock; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->authorizationMock = $this->createMock(Authorization::class); + $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [ + 'authorization' => $this->authorizationMock + ]); + $this->massScheduleMock = $this->createMock(MassSchedule::class); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + } + + /** + * Verify that only authorized request will be able to change groupId + * + * @param int $groupId + * @param int $customerId + * @param bool $isAllowed + * @param int $willThrowException + * @return void + * @throws AuthorizationException + * @dataProvider customerDataProvider + */ + public function testBeforePublishMass( + int $groupId, + int $customerId, + bool $isAllowed, + int $willThrowException + ): void { + if ($willThrowException) { + $this->expectException(AuthorizationException::class); + } else { + $this->expectNotToPerformAssertions(); + } + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $customer->method('getGroupId')->willReturn($groupId); + $customer->method('getId')->willReturn($customerId); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + $entitiesArray = [ + [$customer, 'Password1', ''] + ]; + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with('Magento_Customer::manage') + ->willReturn($isAllowed); + $this->plugin->beforePublishMass( + $this->massScheduleMock, + 'async.magento.customer.api.accountmanagementinterface.createaccount.post', + $entitiesArray, + '', + '' + ); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + [3, 1, false, 1], + [3, 1, true, 0] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php new file mode 100644 index 0000000000000..cda66041ab3c5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Plugin\Webapi\Controller\Rest; + +use Exception; +use Magento\Framework\App\ObjectManager; +use Magento\Customer\Plugin\Webapi\Controller\Rest\ValidateCustomerData; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +/** + * Unit test for ValidateCustomerData plugin + */ +class ValidateCustomerDataTest extends TestCase +{ + + /** + * @var ValidateCustomerData + */ + private $validateCustomerDataObject; + + /** + * @var ReflectionClass + * + */ + private $reflectionObject; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->validateCustomerDataObject = ObjectManager::getInstance()->get(ValidateCustomerData::class); + $this->reflectionObject = new ReflectionClass(get_class($this->validateCustomerDataObject)); + } + + /** + * Test if the customer Info is valid + * + * @param array $customerInfo + * @param array $result + * @dataProvider dataProviderInputData + * @throws Exception + */ + public function testValidateInputData(array $customerInfo, array $result) + { + $this->assertEquals( + $result, + $this->invokeValidateInputData('validateInputData', [$customerInfo]) + ); + } + + /** + * @param string $methodName + * @param array $arguments + * @return mixed + * @throws Exception + */ + private function invokeValidateInputData(string $methodName, array $arguments = []) + { + $validateInputDataMethod = $this->reflectionObject->getMethod($methodName); + $validateInputDataMethod->setAccessible(true); + return $validateInputDataMethod->invokeArgs($this->validateCustomerDataObject, $arguments); + } + + /** + * @return array + */ + public function dataProviderInputData(): array + { + return [ + [ + ['customer' => + [ + 'id' => -1, + 'Id' => 1, + 'name' => + [ + 'firstName' => 'Test', + 'LastName' => 'user' + ], + 'isHavingOwnHouse' => 1, + 'address' => + [ + 'street' => '1st Street', + 'Street' => '3rd Street', + 'city' => 'London' + ], + ] + ], + ['customer' => + [ + 'id' => -1, + 'name' => + [ + 'firstName' => 'Test', + 'LastName' => 'user' + ], + 'isHavingOwnHouse' => 1, + 'address' => + [ + 'street' => '1st Street', + 'city' => 'London' + ], + ] + ], + ] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php new file mode 100644 index 0000000000000..b84e78ca35913 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\ViewModel\Customer; + +use Magento\Customer\ViewModel\Customer\Auth; +use Magento\Framework\App\Http\Context; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthTest extends TestCase +{ + /** + * @var Context|MockObject + */ + private mixed $contextMock; + + /** + * @var Auth + */ + private Auth $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Auth( + $this->contextMock + ); + parent::setUp(); + } + + /** + * Test is logged in value. + * + * @return void + */ + public function testIsLoggedIn(): void + { + $this->contextMock->expects($this->once()) + ->method('getValue') + ->willReturn(true); + + $this->assertEquals( + true, + $this->model->isLoggedIn() + ); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php new file mode 100644 index 0000000000000..bf259040aaf91 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\ViewModel\Customer; + +use Magento\Customer\ViewModel\Customer\JsonSerializer; +use Magento\Framework\Serialize\Serializer\Json as Json; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class JsonSerializerTest extends TestCase +{ + /** + * @var Json|MockObject + */ + private mixed $jsonEncoderMock; + + /** + * @var JsonSerializer + */ + private JsonSerializer $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->jsonEncoderMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new JsonSerializer( + $this->jsonEncoderMock + ); + parent::setUp(); + } + + /** + * Test serialize value. + * + * @return void + */ + public function testSerialize(): void + { + $this->jsonEncoderMock->expects($this->once()) + ->method('serialize') + ->willReturnCallback( + function ($value) { + return json_encode($value); + } + ); + + $this->assertEquals( + json_encode( + [ + 'http://example.com/customer/section/load/' + ] + ), + $this->model->serialize(['http://example.com/customer/section/load/']) + ); + } +} diff --git a/app/code/Magento/Customer/ViewModel/Customer/Auth.php b/app/code/Magento/Customer/ViewModel/Customer/Auth.php new file mode 100644 index 0000000000000..e8c9210d32e12 --- /dev/null +++ b/app/code/Magento/Customer/ViewModel/Customer/Auth.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\ViewModel\Customer; + +use Magento\Customer\Model\Context; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer's auth view model + */ +class Auth implements ArgumentInterface +{ + /** + * @param HttpContext $httpContext + */ + public function __construct( + private HttpContext $httpContext + ) { + } + + /** + * Check is user login + * + * @return bool + */ + public function isLoggedIn(): bool + { + return $this->httpContext->getValue(Context::CONTEXT_AUTH) ?? false; + } +} diff --git a/app/code/Magento/Customer/ViewModel/Customer/Data.php b/app/code/Magento/Customer/ViewModel/Customer/Data.php deleted file mode 100644 index 8c285b368c961..0000000000000 --- a/app/code/Magento/Customer/ViewModel/Customer/Data.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\Customer\ViewModel\Customer; - -use Magento\Customer\Model\Context; -use Magento\Framework\App\Http\Context as HttpContext; -use Magento\Framework\Serialize\Serializer\Json as Json; -use Magento\Framework\View\Element\Block\ArgumentInterface; - -/** - * Customer's data view model - */ -class Data implements ArgumentInterface -{ - /** - * @var Json - */ - private $jsonEncoder; - - /** - * - * @var HttpContext - */ - private $httpContext; - - /** - * @param HttpContext $httpContext - * @param Json $jsonEncoder - */ - public function __construct( - HttpContext $httpContext, - Json $jsonEncoder - ) { - $this->httpContext = $httpContext; - $this->jsonEncoder = $jsonEncoder; - } - - /** - * Check is user login - * - * @return bool - */ - public function isLoggedIn() - { - return $this->httpContext->getValue(Context::CONTEXT_AUTH); - } - - /** - * Encode the mixed $valueToEncode into the JSON format - * - * @param mixed $valueToEncode - * @return string - */ - public function jsonEncode($valueToEncode) - { - return $this->jsonEncoder->serialize($valueToEncode); - } -} diff --git a/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php new file mode 100644 index 0000000000000..c7a7be29a2943 --- /dev/null +++ b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\ViewModel\Customer; + +use Magento\Framework\Serialize\Serializer\Json as Json; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer's json serializer view model + */ +class JsonSerializer implements ArgumentInterface +{ + /** + * @param Json $jsonEncoder + */ + public function __construct( + private Json $jsonEncoder + ) { + } + + /** + * Encode the mixed $value into the JSON format + * + * @param mixed $value + * @return string + */ + public function serialize(mixed $value): string + { + return $this->jsonEncoder->serialize($value); + } +} diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index ef2047644759b..39c82c20f2ec8 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -29,7 +29,8 @@ "suggest": { "magento/module-cookie": "*", "magento/module-customer-sample-data": "*", - "magento/module-webapi": "*" + "magento/module-webapi": "*", + "magento/module-asynchronous-operations": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index 569f9d09c2087..ec76e09fdf459 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -193,6 +193,10 @@ <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> + <field id="confirm" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Require email confirmation if email has been changed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="address" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Name and Address Options</label> diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml index 22596e0b901b2..23a7c9ebb4034 100644 --- a/app/code/Magento/Customer/etc/config.xml +++ b/app/code/Magento/Customer/etc/config.xml @@ -32,6 +32,7 @@ <account_information> <change_email_template>customer_account_information_change_email_template</change_email_template> <change_email_and_password_template>customer_account_information_change_email_and_password_template</change_email_and_password_template> + <confirm>0</confirm> </account_information> <password> <forgot_email_identity>support</forgot_email_identity> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index b178f51f89199..96fd4b86be702 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -585,4 +585,9 @@ </argument> </arguments> </type> + <type name="Magento\AsynchronousOperations\Model\MassSchedule"> + <plugin name="anonymousAsyncCustomerRequest" + type="Magento\Customer\Plugin\AsyncRequestCustomerGroupAuthorization" + /> + </type> </config> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 04ffc5d684b1a..827a153e94674 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -127,7 +127,7 @@ </argument> </arguments> </type> - <type name="Magento\Framework\App\FrontControllerInterface"> - <plugin name="delete-cookie-when-customer-not-exist" type="Magento\Customer\Model\App\FrontController\DeleteCookieWhenCustomerNotExistPlugin"/> + <type name="Magento\Customer\Model\Session"> + <plugin name="afterLogout" type="Magento\Customer\Model\Plugin\ClearSessionsAfterLogoutPlugin"/> </type> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index 18627b68320ed..c5d7a28a3651d 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -31,6 +31,9 @@ </argument> </arguments> </type> + <type name="Magento\Webapi\Controller\Rest\ParamsOverrider"> + <plugin name="validateCustomerData" type="Magento\Customer\Plugin\Webapi\Controller\Rest\ValidateCustomerData" sortOrder="1" disabled="false" /> + </type> <preference for="Magento\Customer\Api\AccountManagementInterface" type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js index 755a8e6df3dbe..3d1132332d70a 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js @@ -21,9 +21,16 @@ define([ setDifferedFromDefault: function (value) { this._super(); - if (parseFloat(value)) { - this.source.set(this.regionScope, this.indexedOptions[value].label); - } + const indexedOptionsArray = Object.values(this.indexedOptions), + countryId = this.source.data.country_id, + hasRegionList = indexedOptionsArray.some(option => option.country_id === countryId); + + this.source.set( + this.regionScope, + hasRegionList + ? parseFloat(value) ? this.indexedOptions?.[value]?.label || '' : '' + : this.source.data?.region || '' + ); } }); }); diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml index eba504a12a1e5..11285070e002e 100644 --- a/app/code/Magento/Customer/view/frontend/layout/default.xml +++ b/app/code/Magento/Customer/view/frontend/layout/default.xml @@ -50,7 +50,8 @@ <block name="customer.customer.data" class="Magento\Customer\Block\CustomerData" template="Magento_Customer::js/customer-data.phtml"> <arguments> - <argument name="view_model" xsi:type="object">Magento\Customer\ViewModel\Customer\Data</argument> + <argument name="auth" xsi:type="object">Magento\Customer\ViewModel\Customer\Auth</argument> + <argument name="json_serializer" xsi:type="object">Magento\Customer\ViewModel\Customer\JsonSerializer</argument> </arguments> </block> <block name="customer.data.invalidation.rules" class="Magento\Customer\Block\CustomerScopeData" diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml index 7031778a8d473..a1df853cc71bf 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml @@ -9,8 +9,11 @@ use Magento\Framework\App\ObjectManager; /** @var \Magento\Customer\Block\CustomerData $block */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper -/** @var Data $viewModel */ -$viewModel = $block->getViewModel() ?? ObjectManager::getInstance()->get(Data::class); +/** @var Auth $auth */ +$auth = $block->getAuth() ?? ObjectManager::getInstance()->get(Auth::class); +/** @var JsonSerializer $jsonSerializer */ +$jsonSerializer = $block->getJsonSerializer() ?? + ObjectManager::getInstance()->get(JsonSerializer::class); $customerDataUrl = $block->getCustomerDataUrl('customer/account/updateSession'); $expirableSectionNames = $block->getExpirableSectionNames(); ?> @@ -20,10 +23,12 @@ $expirableSectionNames = $block->getExpirableSectionNames(); "Magento_Customer/js/customer-data": { "sectionLoadUrl": "<?= $block->escapeJs($block->getCustomerDataUrl('customer/section/load')) ?>", "expirableSectionLifetime": <?= (int)$block->getExpirableSectionLifetime() ?>, - "expirableSectionNames": <?= /* @noEscape */ $viewModel->jsonEncode($expirableSectionNames) ?>, + "expirableSectionNames": <?= /* @noEscape */ $jsonSerializer->serialize( + $expirableSectionNames + ) ?>, "cookieLifeTime": "<?= $block->escapeJs($block->getCookieLifeTime()) ?>", "updateSessionUrl": "<?= $block->escapeJs($customerDataUrl) ?>", - "isLoggedIn": "<?= /* @noEscape */ $viewModel->isLoggedIn() ?>" + "isLoggedIn": "<?= /* @noEscape */ $auth->isLoggedIn() ?>" } } } diff --git a/app/code/Magento/CustomerAnalytics/README.md b/app/code/Magento/CustomerAnalytics/README.md index b9cc560cea7e0..153379cd97679 100644 --- a/app/code/Magento/CustomerAnalytics/README.md +++ b/app/code/Magento/CustomerAnalytics/README.md @@ -14,5 +14,6 @@ For information about a module installation in Magento 2, see [Enable or disable ## Additional data More information can get at articles: + - [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/) - [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/) diff --git a/app/code/Magento/CustomerDownloadableGraphQl/README.md b/app/code/Magento/CustomerDownloadableGraphQl/README.md index 2a3729b36007e..28d777e27cb09 100644 --- a/app/code/Magento/CustomerDownloadableGraphQl/README.md +++ b/app/code/Magento/CustomerDownloadableGraphQl/README.md @@ -19,7 +19,7 @@ Extension developers can interact with the Magento_CatalogGraphQl module. For mo ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query diff --git a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php index b3ae57e0ff994..0140bcd3739cb 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php +++ b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php @@ -34,11 +34,6 @@ class AddUserInfoToContext implements UserContextParametersProcessorInterface */ private $customerRepository; - /** - * @var CustomerInterface|null - */ - private $loggedInCustomerData = null; - /** * @param UserContextInterface $userContext * @param Session $session @@ -82,10 +77,6 @@ public function execute(ContextParametersInterface $contextParameters): ContextP $isCustomer = $this->isCustomer($currentUserId, $currentUserType); $contextParameters->addExtensionAttribute('is_customer', $isCustomer); - if ($this->session->isLoggedIn()) { - $this->loggedInCustomerData = $this->session->getCustomerData(); - } - if ($isCustomer) { $customer = $this->customerRepository->getById($currentUserId); $this->session->setCustomerData($customer); @@ -101,7 +92,7 @@ public function execute(ContextParametersInterface $contextParameters): ContextP */ public function getLoggedInCustomerData(): ?CustomerInterface { - return $this->loggedInCustomerData; + return $this->session->isLoggedIn() ? $this->session->getCustomerData() : null; } /** diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php index 5a302f4c3df27..6bb4a81182521 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -7,14 +7,16 @@ namespace Magento\CustomerGraphQl\Model\Customer\Address; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Framework\Api\CustomAttributesDataInterface; -use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; use Magento\Customer\Model\CustomerFactory; -use Magento\Framework\Webapi\ServiceOutputProcessor; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Webapi\ServiceOutputProcessor; /** * Transform single customer address data from object to in array format @@ -41,22 +43,30 @@ class ExtractCustomerAddressData */ private $customerFactory; + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + /** * @param ServiceOutputProcessor $serviceOutputProcessor * @param SerializerInterface $jsonSerializer * @param CustomerResourceModel $customerResourceModel * @param CustomerFactory $customerFactory + * @param GetAttributeValueInterface $getAttributeValue */ public function __construct( ServiceOutputProcessor $serviceOutputProcessor, SerializerInterface $jsonSerializer, CustomerResourceModel $customerResourceModel, - CustomerFactory $customerFactory + CustomerFactory $customerFactory, + GetAttributeValueInterface $getAttributeValue ) { $this->serviceOutputProcessor = $serviceOutputProcessor; $this->jsonSerializer = $jsonSerializer; $this->customerResourceModel = $customerResourceModel; $this->customerFactory = $customerFactory; + $this->getAttributeValue = $getAttributeValue; } /** @@ -100,31 +110,11 @@ public function execute(AddressInterface $address): array $addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY] ); } - $customAttributes = []; - if (isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { - foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - // @ignoreCoverageStart - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - // @ignoreCoverageEnd - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } - } + + $customAttributes = isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) + ? $this->formatCustomAttributes($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) + : ['custom_attributesV2' => []]; + $addressData = array_merge($addressData, $customAttributes); $addressData['customer_id'] = null; @@ -135,4 +125,54 @@ public function execute(AddressInterface $address): array return $addressData; } + + /** + * Retrieve formatted custom attributes + * + * @param array $attributes + * @return array + */ + private function formatCustomAttributes(array $attributes) + { + foreach ($attributes as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + // @ignoreCoverageStart + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + // @ignoreCoverageEnd + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + + $customAttributes['custom_attributesV2'] = array_map( + function (array $customAttribute) { + return $this->getAttributeValue->execute( + AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + $customAttribute['attribute_code'], + $customAttribute['value'] + ); + }, + $attributes + ); + usort($customAttributes['custom_attributesV2'], function (array $a, array $b) { + $aPosition = $a['sort_order']; + $bPosition = $b['sort_order']; + return $aPosition <=> $bPosition; + }); + + return $customAttributes; + } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php index a631b7ba86194..971b0352b8931 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php @@ -122,6 +122,7 @@ private function createAccount(array $data, StoreInterface $store): CustomerInte $customerDataObject, CustomerInterface::class ); + $data = array_merge($requiredDataAttributes, $data); $this->validateCustomerData->execute($data); $this->dataObjectHelper->populateWithArray( diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php index c62a931809644..01bb007ef618a 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -7,11 +7,12 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\EavGraphQl\Model\GetAttributeValueComposite; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Webapi\ServiceOutputProcessor; -use Magento\Customer\Api\Data\CustomerInterface; /** * Transform single customer data from object to in array format @@ -24,20 +25,20 @@ class ExtractCustomerData private $serviceOutputProcessor; /** - * @var SerializerInterface + * @var GetAttributeValueComposite */ - private $serializer; + private GetAttributeValueComposite $getAttributeValueComposite; /** * @param ServiceOutputProcessor $serviceOutputProcessor - * @param SerializerInterface $serializer + * @param GetAttributeValueComposite $getAttributeValueComposite */ public function __construct( ServiceOutputProcessor $serviceOutputProcessor, - SerializerInterface $serializer + GetAttributeValueComposite $getAttributeValueComposite ) { $this->serviceOutputProcessor = $serviceOutputProcessor; - $this->serializer = $serializer; + $this->getAttributeValueComposite = $getAttributeValueComposite; } /** @@ -77,30 +78,24 @@ public function execute(CustomerInterface $customer): array if (isset($customerData['extension_attributes'])) { $customerData = array_merge($customerData, $customerData['extension_attributes']); } - $customAttributes = []; if (isset($customerData['custom_attributes'])) { - foreach ($customerData['custom_attributes'] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } + $customerData['custom_attributes'] = array_map( + function (array $customAttribute) { + return $this->getAttributeValueComposite->execute( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + $customAttribute + ); + }, + $customerData['custom_attributes'] + ); + usort($customerData['custom_attributes'], function (array $a, array $b) { + $aPosition = $a['sort_order']; + $bPosition = $b['sort_order']; + return $aPosition <=> $bPosition; + }); + } else { + $customerData['custom_attributes'] = []; } - $customerData = array_merge($customerData, $customAttributes); //Fields are deprecated and should not be exposed on storefront. $customerData['group_id'] = null; $customerData['id'] = null; diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php new file mode 100644 index 0000000000000..8c476abba90bc --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Customer\Api\MetadataInterface; +use Magento\EavGraphQl\Model\GetAttributesFormInterface; + +/** + * Attributes form provider for customer + */ +class GetAttributesForm implements GetAttributesFormInterface +{ + /** + * @var MetadataInterface + */ + private MetadataInterface $entity; + + /** + * @var string + */ + private string $type; + + /** + * @param MetadataInterface $metadata + * @param string $type + */ + public function __construct(MetadataInterface $metadata, string $type) + { + $this->entity = $metadata; + $this->type = $type; + } + + /** + * @inheritDoc + */ + public function execute(string $formCode): ?array + { + $attributes = []; + foreach ($this->entity->getAttributes($formCode) as $attribute) { + $attributes[] = ['entity_type' => $this->type, 'attribute_code' => $attribute->getAttributeCode()]; + } + return $attributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php new file mode 100644 index 0000000000000..dc46f08fd0434 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\GetAttributeSelectedOptionComposite; +use Magento\EavGraphQl\Model\GetAttributeValueInterface; + +/** + * Custom attribute value provider for customer + */ +class GetCustomAttributes implements GetAttributeValueInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var GetAttributeSelectedOptionComposite + */ + private GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + $this->attributeSelectedOptionComposite = $attributeSelectedOptionComposite; + } + + /** + * @inheritDoc + */ + public function execute(string $entityType, array $customAttribute): ?array + { + $attr = $this->attributeRepository->get( + $entityType, + $customAttribute['attribute_code'] + ); + + $result = [ + 'entity_type' => $entityType, + 'code' => $customAttribute['attribute_code'], + 'sort_order' => $attr->getSortOrder() ?? '' + ]; + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + $result['selected_options'] = $this->attributeSelectedOptionComposite->execute( + $entityType, + $customAttribute + ); + } else { + $result['value'] = $customAttribute['value']; + } + return $result; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php new file mode 100644 index 0000000000000..8724e57ff11c5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\GetAttributeSelectedOptionInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Custom attribute value provider for customer + */ +class GetCustomSelectedOptionAttributes implements GetAttributeSelectedOptionInterface +{ + /** + * @var Uid + */ + private Uid $uid; + + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @param Uid $uid + * @param AttributeRepository $attributeRepository + */ + public function __construct( + Uid $uid, + AttributeRepository $attributeRepository + ) { + $this->uid = $uid; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $entityType, array $customAttribute): ?array + { + $attr = $this->attributeRepository->get( + $entityType, + $customAttribute['attribute_code'] + ); + + $result = []; + $selectedValues = explode(',', $customAttribute['value']); + foreach ($attr->getOptions() as $option) { + if (!in_array($option->getValue(), $selectedValues)) { + continue; + } + $result[] = [ + 'uid' => $this->uid->encode($option->getValue()), + 'value' => $option->getValue(), + 'label' => $option->getLabel() + ]; + } + return $result; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php index d82b8c6f941fa..8ff74178f2be8 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php @@ -8,6 +8,7 @@ namespace Magento\CustomerGraphQl\Model\Customer; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; @@ -89,17 +90,20 @@ public function __construct( * @throws GraphQlAuthenticationException * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException + * @throws NoSuchEntityException + * @throws LocalizedException */ public function execute(CustomerInterface $customer, array $data, StoreInterface $store): void { if (isset($data['email']) && $customer->getEmail() !== $data['email']) { - if (!isset($data['password']) || empty($data['password'])) { + if (empty($data['password'])) { throw new GraphQlInputException(__('Provide the current "password" to change "email".')); } $this->checkCustomerPassword->execute($data['password'], (int)$customer->getId()); $customer->setEmail($data['email']); } + $this->validateCustomerData->execute($data); $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); diff --git a/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php new file mode 100644 index 0000000000000..bbc8b2eaab041 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Output; + +use Magento\Customer\Api\MetadataInterface; +use Magento\Customer\Model\Data\ValidationRule; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\EnumLookup; + +/** + * Format attributes metadata for GraphQL output + */ +class CustomerAttributeMetadata implements GetAttributeDataInterface +{ + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @var MetadataInterface + */ + private MetadataInterface $metadata; + + /** + * @var string + */ + private string $entityType; + + /** + * @param EnumLookup $enumLookup + * @param MetadataInterface $metadata + * @param string $entityType + */ + public function __construct( + EnumLookup $enumLookup, + MetadataInterface $metadata, + string $entityType + ) { + $this->enumLookup = $enumLookup; + $this->metadata = $metadata; + $this->entityType = $entityType; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws LocalizedException + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + if ($entityType !== $this->entityType) { + return []; + } + + $attributeMetadata = $this->metadata->getAttributeMetadata($attribute->getAttributeCode()); + $data = []; + + $validationRules = array_map(function (ValidationRule $validationRule) { + return [ + 'name' => $this->enumLookup->getEnumValueFromField( + 'ValidationRuleEnum', + strtoupper($validationRule->getName()) + ), + 'value' => $validationRule->getValue() + ]; + }, $attributeMetadata->getValidationRules()); + + if ($attributeMetadata->isVisible()) { + $data = [ + 'input_filter' => empty($attributeMetadata->getInputFilter()) + ? 'NONE' + : $this->enumLookup->getEnumValueFromField( + 'InputFilterEnum', + strtoupper($attributeMetadata->getInputFilter()) + ), + 'multiline_count' => $attributeMetadata->getMultilineCount(), + 'sort_order' => $attributeMetadata->getSortOrder(), + 'validate_rules' => $validationRules, + 'attributeMetadata' => $attributeMetadata + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php new file mode 100644 index 0000000000000..03332301706ce --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\Address; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Provides the customer record identity to invalidate on address change. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', Customer::ENTITY, $object->getCustomerId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php new file mode 100644 index 0000000000000..db67d2e860c44 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Data\Customer; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\Framework\EntityManager\TypeResolver; +use Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorInterface; + +/** + * Customer resolver data dehydrator to create snapshot data necessary to restore model. + */ +class ModelDehydrator implements DehydratorInterface +{ + /** + * @var TypeResolver + */ + private TypeResolver $typeResolver; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param HydratorPool $hydratorPool + * @param TypeResolver $typeResolver + */ + public function __construct( + HydratorPool $hydratorPool, + TypeResolver $typeResolver + ) { + $this->typeResolver = $typeResolver; + $this->hydratorPool = $hydratorPool; + } + + /** + * @inheritdoc + */ + public function dehydrate(array &$resolvedValue): void + { + if (isset($resolvedValue['model']) && $resolvedValue['model'] instanceof Customer) { + /** @var Customer $model */ + $model = $resolvedValue['model']; + $entityType = $this->typeResolver->resolve($model); + $resolvedValue['model_data'] = $this->hydratorPool->getHydrator($entityType) + ->extract($model); + $resolvedValue['model_entity_type'] = $entityType; + $resolvedValue['model_id'] = $model->getId(); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php new file mode 100644 index 0000000000000..4b4c187bbd949 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Data\Customer; +use Magento\Customer\Model\Data\CustomerFactory; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorInterface; + +/** + * Customer resolver data hydrator to rehydrate propagated model. + */ +class ModelHydrator implements HydratorInterface +{ + /** + * @var CustomerFactory + */ + private CustomerFactory $customerFactory; + + /** + * @var Customer[] + */ + private array $customerModels = []; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param CustomerFactory $customerFactory + * @param HydratorPool $hydratorPool + */ + public function __construct( + CustomerFactory $customerFactory, + HydratorPool $hydratorPool + ) { + $this->hydratorPool = $hydratorPool; + $this->customerFactory = $customerFactory; + } + + /** + * @inheritdoc + */ + public function hydrate(array &$resolverData): void + { + if (isset($this->customerModels[$resolverData['model_id']])) { + $resolverData['model'] = $this->customerModels[$resolverData['model_id']]; + } else { + $hydrator = $this->hydratorPool->getHydrator($resolverData['model_entity_type']); + $model = $this->customerFactory->create(); + $hydrator->hydrate($model, $resolverData['model_data']); + $this->customerModels[$resolverData['model_id']] = $model; + $resolverData['model'] = $this->customerModels[$resolverData['model_id']]; + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..85f659cc0adce --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Customer; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved Customer for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Customer::ENTITY; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($resolvedData['model']->getId()) ? + [] : [sprintf('%s_%s', $this->cacheTag, $resolvedData['model']->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php new file mode 100644 index 0000000000000..f1d6406295c53 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Customer entity tag resolver strategy. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', Customer::ENTITY, $object->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..f5e10440ddddf --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved Customer subscription status for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = 'SUBSCRIBER'; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($parentResolvedData['model']->getId()) ? + [] : [sprintf('%s_%s', $this->cacheTag, $parentResolvedData['model']->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php new file mode 100644 index 0000000000000..7b953b2534513 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Customer subscriber entity tag resolver strategy. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', "SUBSCRIBER", $object->getCustomerId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php new file mode 100644 index 0000000000000..75493acba88a1 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides logged-in customer id as a factor to use in the cache key for resolver cache. + */ +class CurrentCustomerId implements GenericFactorProviderInterface +{ + /** + * Factor name. + */ + private const NAME = "CUSTOMER_ID"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritDoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)$context->getUserId(); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php new file mode 100644 index 0000000000000..33333eb8cf686 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides customer group as a factor to use in the cache key for resolver cache. + */ +class CustomerGroup implements GenericFactorProviderInterface +{ + private const NAME = "CUSTOMER_GROUP"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)($context->getExtensionAttributes()->getCustomerGroupId() + ?? GroupInterface::NOT_LOGGED_IN_ID); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php new file mode 100644 index 0000000000000..5463c90894ef8 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Model\ResourceModel\GroupRepository as CustomerGroupRepository; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; +use Magento\Tax\Model\Calculation as CalculationModel; +use Magento\Tax\Model\ResourceModel\Calculation as CalculationResource; + +/** + * Provides tax rate as a factor to use in the cache key for resolver cache. + */ +class CustomerTaxRate implements GenericFactorProviderInterface +{ + private const NAME = 'CUSTOMER_TAX_RATE'; + + /** + * @var CustomerGroupRepository + */ + private $groupRepository; + + /** + * @var CalculationModel + */ + private $calculationModel; + + /** + * @var CalculationResource + */ + private $calculationResource; + + /** + * @param CustomerGroupRepository $groupRepository + * @param CalculationModel $calculationModel + * @param CalculationResource $calculationResource + */ + public function __construct( + CustomerGroupRepository $groupRepository, + CalculationModel $calculationModel, + CalculationResource $calculationResource + ) { + $this->groupRepository = $groupRepository; + $this->calculationModel = $calculationModel; + $this->calculationResource = $calculationResource; + } + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + $customerId = $context->getExtensionAttributes()->getIsCustomer() + ? (int)$context->getUserId() + : 0; + $customerTaxClassId = $this->groupRepository->getById( + $context->getExtensionAttributes()->getCustomerGroupId() ?? GroupInterface::NOT_LOGGED_IN_ID + )->getTaxClassId(); + $rateRequest = $this->calculationModel->getRateRequest( + null, + null, + $customerTaxClassId, + $context->getExtensionAttributes()->getStore(), + $customerId + ); + $rateInfo = $this->calculationResource->getRateInfo($rateRequest); + return (string)$rateInfo['value']; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php new file mode 100644 index 0000000000000..a8207232b177b --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides logged-in status as a factor to use in the cache key for resolver cache. + */ +class IsLoggedIn implements GenericFactorProviderInterface +{ + private const NAME = "IS_LOGGED_IN"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return $context->getExtensionAttributes()->getIsCustomer() ? "true" : "false"; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php new file mode 100644 index 0000000000000..2030c24fb1840 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\ParentValueFactorProviderInterface; + +/** + * Provides customer id from the parent resolved value as a factor to use in the cache key for resolver cache. + */ +class ParentCustomerEntityId implements ParentValueFactorProviderInterface +{ + /** + * Factor name. + */ + private const NAME = "PARENT_ENTITY_CUSTOMER_ID"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritDoc + */ + public function getFactorValue(ContextInterface $context, array $parentValue): string + { + if (isset($parentValue['model_id'])) { + return (string)$parentValue['model_id']; + } elseif (isset($parentValue['model']) && $parentValue['model'] instanceof CustomerInterface) { + return (string)$parentValue['model']->getId(); + } + throw new \InvalidArgumentException(__CLASS__ . " factor provider requires parent value " . + "to contain customer model id or customer model."); + } + + /** + * @inheritDoc + */ + public function isRequiredOrigData(): bool + { + return false; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php new file mode 100755 index 0000000000000..7850134e45f38 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver Custom Attribute filter + */ +class CustomAttributeFilter implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + $customAttributes = $value[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]; + if (!empty($args['attributeCodes'])) { + $attributeCodes = array_values($args['attributeCodes']); + return array_filter($customAttributes, function ($attr) use ($attributeCodes) { + return in_array($attr['code'], $attributeCodes); + }); + } + + return $customAttributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php new file mode 100755 index 0000000000000..187e54821307e --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver Customer Address Custom Attribute filter + */ +class CustomerAddressCustomAttributeFilter implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + $customAttributes = $value[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES . 'V2']; + if (isset($args['attributeCodes']) && !empty($args['attributeCodes'])) { + $attributeCodes = array_values($args['attributeCodes']); + return array_filter($customAttributes, function ($attr) use ($attributeCodes) { + return in_array($attr['code'], $attributeCodes); + }); + } + + return $customAttributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php index 8cdf6518a4ef3..4d6c199dab767 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php @@ -7,7 +7,7 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Data\Customer; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php index a2b8c3fa78a2c..af6a8bf671a7f 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php @@ -65,7 +65,6 @@ public function resolve( if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } - $isSecure = $this->registry->registry('isSecureArea'); $this->registry->unregister('isSecureArea'); diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php index b16ce7ee710ad..e39ae2ba17db4 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php @@ -56,9 +56,7 @@ public function resolve( if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - /** @var CustomerInterface $customer */ - $customer = $value['model']; - $customerId = (int)$customer->getId(); + $customerId = (int)$value['model']->getId(); $extensionAttributes = $context->getExtensionAttributes(); if (!$extensionAttributes) { diff --git a/app/code/Magento/CustomerGraphQl/README.md b/app/code/Magento/CustomerGraphQl/README.md index ae374c045bae0..8f5df3db3b647 100644 --- a/app/code/Magento/CustomerGraphQl/README.md +++ b/app/code/Magento/CustomerGraphQl/README.md @@ -23,7 +23,7 @@ Extension developers can interact with the Magento_CustomerGraphQl module. For m ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query diff --git a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php index 4699af2b9c389..4854efb5a1cd1 100644 --- a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php +++ b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php @@ -84,14 +84,6 @@ public function testExecuteForCustomer(): void $this->contextParametersMock ->expects($this->once()) ->method('setUserType'); - $this->sessionMock - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->sessionMock - ->expects($this->once()) - ->method('getCustomerData') - ->willReturn($this->customerMock); $this->customerRepositoryMock ->expects($this->once()) ->method('getById') diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 5967d2e9f8ac7..9fb9668de0e77 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -7,6 +7,7 @@ "magento/module-authorization": "*", "magento/module-customer": "*", "magento/module-eav": "*", + "magento/module-eav-graph-ql": "*", "magento/module-graph-ql": "*", "magento/module-newsletter": "*", "magento/module-integration": "*", @@ -14,7 +15,8 @@ "magento/framework": "*", "magento/module-directory": "*", "magento/module-tax": "*", - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/CustomerGraphQl/etc/di.xml b/app/code/Magento/CustomerGraphQl/etc/di.xml new file mode 100644 index 0000000000000..6fbc996079086 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/etc/di.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver"> + <arguments> + <argument name="invalidatableObjectTypes" xsi:type="array"> + <item name="Magento\Customer\Model\Customer" xsi:type="string"> + Magento\Customer\Model\Customer + </item> + <item name="Magento\Customer\Model\Address" xsi:type="string"> + Magento\Customer\Model\Address + </item> + <item name="Magento\Newsletter\Model\Subscriber" xsi:type="string"> + Magento\Newsletter\Model\Subscriber + </item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Cache\Tag\Strategy\Factory"> + <arguments> + <argument name="customStrategies" xsi:type="array"> + <item name="Magento\Customer\Model\Customer" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\TagsStrategy + </item> + <item name="Magento\Customer\Model\Address" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\Address\TagsStrategy + </item> + <item name="Magento\Newsletter\Model\Subscriber" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\TagsStrategy + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1e616e37a12f5..be4f38d80e546 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -40,6 +40,34 @@ </argument> </arguments> </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeMetadata"> + <arguments> + <argument name="entityTypes" xsi:type="array"> + <item name="CUSTOMER" xsi:type="string">CustomerAttributeMetadata</item> + <item name="CUSTOMER_ADDRESS" xsi:type="string">CustomerAttributeMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">GetCustomerAttributesMetadata</item> + <item name="customer_address" xsi:type="object">GetCustomerAddressAttributesMetadata</item> + </argument> + </arguments> + </type> + <virtualType name="GetCustomerAttributesMetadata" type="Magento\CustomerGraphQl\Model\Output\CustomerAttributeMetadata"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Model\Metadata\CustomerMetadata</argument> + <argument name="entityType" xsi:type="string">customer</argument> + </arguments> + </virtualType> + <virtualType name="GetCustomerAddressAttributesMetadata" type="Magento\CustomerGraphQl\Model\Output\CustomerAttributeMetadata"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Model\Metadata\AddressMetadata</argument> + <argument name="entityType" xsi:type="string">customer_address</argument> + </arguments> + </virtualType> <!-- Validate input customer data --> <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> <arguments> @@ -62,4 +90,137 @@ </argument> </arguments> </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator"> + <arguments> + <argument name="factorProviders" xsi:type="array"> + <item name="customergroup" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerGroup</item> + <item name="customertaxrate" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerTaxRate</item> + <item name="isloggedin" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeEntityTypeEnum" xsi:type="array"> + <item name="customer" xsi:type="string">customer</item> + <item name="customer_address" xsi:type="string">customer_address</item> + </item> + <item name="InputFilterEnum" xsi:type="array"> + <item name="none" xsi:type="string">NONE</item> + <item name="date" xsi:type="string">DATE</item> + <item name="trim" xsi:type="string">TRIM</item> + <item name="striptags" xsi:type="string">STRIPTAGS</item> + <item name="escapehtml" xsi:type="string">ESCAPEHTML</item> + </item> + <item name="ValidationRuleEnum" xsi:type="array"> + <item name="date_range_max" xsi:type="string">DATE_RANGE_MAX</item> + <item name="date_range_min" xsi:type="string">DATE_RANGE_MIN</item> + <item name="file_extensions" xsi:type="string">FILE_EXTENSIONS</item> + <item name="input_validation" xsi:type="string">INPUT_VALIDATION</item> + <item name="max_text_length" xsi:type="string">MAX_TEXT_LENGTH</item> + <item name="min_text_length" xsi:type="string">MIN_TEXT_LENGTH</item> + <item name="max_file_size" xsi:type="string">MAX_FILE_SIZE</item> + <item name="max_image_height" xsi:type="string">MAX_IMAGE_HEGHT</item> + <item name="max_image_width" xsi:type="string">MAX_IMAGE_WIDTH</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\GetAttributesFormComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">GetCustomerAttributesForm</item> + <item name="customer_address" xsi:type="object">GetCustomerAddressAttributesForm</item> + </argument> + </arguments> + </type> + <virtualType name="GetCustomerAttributesForm" type="Magento\CustomerGraphQl\Model\Customer\GetAttributesForm"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Api\CustomerMetadataInterface</argument> + <argument name="type" xsi:type="string">customer</argument> + </arguments> + </virtualType> + <virtualType name="GetCustomerAddressAttributesForm" type="Magento\CustomerGraphQl\Model\Customer\GetAttributesForm"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Api\AddressMetadataInterface</argument> + <argument name="type" xsi:type="string">customer_address</argument> + </arguments> + </virtualType> + <type name="Magento\EavGraphQl\Model\GetAttributeValueComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes</item> + <item name="customer_address" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\GetAttributeSelectedOptionComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes</item> + <item name="customer_address" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeValue"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider"> + <arguments> + <argument name="cacheableResolverClassNameIdentityMap" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="string"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ResolverCacheIdentity + </item> + <item name="Magento\CustomerGraphQl\Model\Resolver\IsSubscribed" xsi:type="string"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\ResolverCacheIdentity + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"> + <arguments> + <argument name="hydratorConfig" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="model_hydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelHydrator</item> + </item> + </item> + </argument> + <argument name="dehydratorConfig" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="model_dehydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelDehydrator</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider"> + <arguments> + <argument name="customFactorProviders" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="current_customer_id" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId</item> + </item> + <item name="Magento\CustomerGraphQl\Model\Resolver\IsSubscribed" xsi:type="array"> + <item name="parent_customer_entity_id" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\ParentCustomerEntityId</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml index b15df7fc0be6b..bdbbaa3e7f432 100644 --- a/app/code/Magento/CustomerGraphQl/etc/module.xml +++ b/app/code/Magento/CustomerGraphQl/etc/module.xml @@ -9,6 +9,8 @@ <module name="Magento_CustomerGraphQl" > <sequence> <module name="Magento_Customer"/> + <module name="Magento_Newsletter"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 439ce4742ca3b..e7e9a1484bb26 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -49,7 +49,8 @@ input CustomerAddressInput @doc(description: "Contains details about a billing o 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: "Deprecated: Custom attributes should not be put into container.") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated. Use custom_attributesV2 instead.") @deprecated(reason: "Use custom_attributesV2 instead.") + custom_attributesV2: [AttributeValueInput] @doc(description: "Custom attributes assigned to the customer address.") } input CustomerAddressRegionInput @doc(description: "Defines the customer's state or province.") { @@ -95,6 +96,7 @@ input CustomerCreateInput @doc(description: "An input object for creating a cus 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.") + custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.") } input CustomerUpdateInput @doc(description: "An input object for updating a customer.") { @@ -108,6 +110,7 @@ input CustomerUpdateInput @doc(description: "An input object for updating a cust prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III.") taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers).") + custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.") } type CustomerOutput @doc(description: "Contains details about a newly-created or updated customer.") { @@ -136,6 +139,7 @@ type Customer @doc(description: "Defines the customer name, addresses, and other 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).") + custom_attributes(attributeCodes: [ID!]): [AttributeValueInterface] @doc(description: "Customer's custom attributes.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomAttributeFilter") } type CustomerAddress @doc(description: "Contains detailed information about a customer's billing or shipping address."){ @@ -159,7 +163,8 @@ type CustomerAddress @doc(description: "Contains detailed information about a cu 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 customer's default shipping address.") default_billing: Boolean @doc(description: "Indicates whether the address is the customer's default billing address.") - custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into a container.") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Use custom_attributesV2 instead.") + custom_attributesV2(attributeCodes: [ID!]): [AttributeValueInterface!]! @doc(description: "Custom attributes assigned to the customer address.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddressCustomAttributeFilter") extension_attributes: [CustomerAddressAttribute] @doc(description: "Contains any extension attributes for the address.") } @@ -171,7 +176,7 @@ type CustomerAddressRegion @doc(description: "Defines the customer's state or pr type CustomerAddressAttribute @doc(description: "Specifies the attribute code and value of a customer address attribute.") { attribute_code: String @doc(description: "The name assigned to the customer address attribute.") - value: String @doc(description: "The valuue assigned to the customer address attribute.") + value: String @doc(description: "The value assigned to the customer address attribute.") } type IsEmailAvailableOutput @doc(description: "Contains the result of the `isEmailAvailable` query.") { @@ -425,3 +430,40 @@ enum CountryCodeEnum @doc(description: "The list of country codes.") { ZM @doc(description: "Zambia") ZW @doc(description: "Zimbabwe") } + +enum AttributeEntityTypeEnum { + CUSTOMER + CUSTOMER_ADDRESS +} + +type CustomerAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Customer attribute metadata.") { + input_filter: InputFilterEnum @doc(description: "The template used for the input of the attribute (e.g., 'date').") + multiline_count: Int @doc(description: "The number of lines of the attribute value.") + sort_order: Int @doc(description: "The position of the attribute in the form.") + validate_rules: [ValidationRule] @doc(description: "The validation rules of the attribute value.") +} + +enum InputFilterEnum @doc(description: "List of templates/filters applied to customer attribute input.") { + NONE @doc(description: "There are no templates or filters to be applied.") + DATE @doc(description: "Forces attribute input to follow the date format.") + TRIM @doc(description: "Strip whitespace (or other characters) from the beginning and end of the input.") + STRIPTAGS @doc(description: "Strip HTML Tags.") + ESCAPEHTML @doc(description: "Escape HTML Entities.") +} + +type ValidationRule @doc(description: "Defines a customer attribute validation rule.") { + name: ValidationRuleEnum @doc(description: "Validation rule name applied to a customer attribute.") + value: String @doc(description: "Validation rule value.") +} + +enum ValidationRuleEnum @doc(description: "List of validation rule names applied to a customer attribute.") { + DATE_RANGE_MAX + DATE_RANGE_MIN + FILE_EXTENSIONS + INPUT_VALIDATION + MAX_TEXT_LENGTH + MIN_TEXT_LENGTH + MAX_FILE_SIZE + MAX_IMAGE_HEIGHT + MAX_IMAGE_WIDTH +} diff --git a/app/code/Magento/CustomerImportExport/README.md b/app/code/Magento/CustomerImportExport/README.md index 16c4189acfe63..50c978eae1a7a 100644 --- a/app/code/Magento/CustomerImportExport/README.md +++ b/app/code/Magento/CustomerImportExport/README.md @@ -15,6 +15,7 @@ Extension developers can interact with the Magento_CustomerImportExport module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `customer_import_export_index_exportcsv` - `customer_import_export_index_exportxml` - `customer_index_grid_block` @@ -24,5 +25,6 @@ For more information about a layout in Magento 2, see the [Layout documentation] ## Additional information You can get more information about import/export processes in magento at the articles: + - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php index 2d8c105d2b29c..10e3b5efcbdb0 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php @@ -38,7 +38,7 @@ class AddressTest extends TestCase /** * Test attribute code */ - const ATTRIBUTE_CODE = 'code1'; + public const ATTRIBUTE_CODE = 'code1'; /** * Websites array (website id => code) @@ -52,10 +52,16 @@ class AddressTest extends TestCase * * @var array */ - protected $_attributes = [['attribute_id' => 1, 'attribute_code' => self::ATTRIBUTE_CODE]]; + protected $_attributes = [ + [ + 'attribute_id' => 1, + 'attribute_code' => self::ATTRIBUTE_CODE, + 'frontend_input' => 'multiselect' + ] + ]; /** - * Customer data + * Customer details * * @var array */ @@ -166,8 +172,11 @@ protected function _getModelDependencies() true, true, true, - ['_construct'] + ['_construct', 'getSource'] ); + + $attributeSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class); + $attribute->expects($this->once())->method('getSource')->willReturn($attributeSource); $attributeCollection->addItem($attribute); } diff --git a/app/code/Magento/Deploy/README.md b/app/code/Magento/Deploy/README.md index 0e4bdb11e0bb8..1d55d55b54e30 100644 --- a/app/code/Magento/Deploy/README.md +++ b/app/code/Magento/Deploy/README.md @@ -1,19 +1,23 @@ # Overview + ## Purpose of module -Deploy is a module that holds collection of services and command line tools to help with Magento application deployment. +Deploy is a module that holds collection of services and command line tools to help with Magento application deployment. To execute this command, please, run "bin/magento setup:static-content:deploy" from the Magento root directory. -Deploy module contains 2 additional commands that allows switching between application modes (for instance from +Deploy module contains 2 additional commands that allows switching between application modes (for instance from development to production) and show current application mode. To change the mode run "bin/magento deploy:mode:set [mode]". Where mode can be one of the following: + - development - production -When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS +When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS and do not run the compilation process. # Deployment + ## System requirements ## Install + The Magento_Deploy module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml index 3a7d3663c8875..8c3e0f750debd 100644 --- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml @@ -14,9 +14,7 @@ <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"/> --> + <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 index bf7014cdbb49d..82ba4102736f7 100644 --- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml @@ -7,16 +7,8 @@ --> <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/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php index b752eaa111fa4..1525637c017a1 100644 --- a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php +++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php @@ -136,6 +136,7 @@ private function persistModule(Schema $schema, string $moduleName) . Diff::GENERATED_WHITELIST_FILE_NAME; //We need to load whitelist file and update it with new revision of code. + // phpcs:disable Magento2.Functions.DiscouragedFunction if (file_exists($whiteListFileName)) { $content = json_decode(file_get_contents($whiteListFileName), true); } @@ -183,6 +184,7 @@ private function getElementsWithFixedName(array $tableData): array * @param string $tableName * @param array $tableData * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function getElementsWithAutogeneratedName(Schema $schema, string $tableName, array $tableData) : array { @@ -192,35 +194,42 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN $elementType = 'index'; if (!empty($tableData[$elementType])) { foreach ($tableData[$elementType] as $tableElementData) { - $indexName = $this->elementNameResolver->getFullIndexName( - $table, - $tableElementData['column'], - $tableElementData['indexType'] ?? null - ); - $declaredStructure[$elementType][$indexName] = true; + if (isset($tableElementData['column'])) { + $indexName = $this->elementNameResolver->getFullIndexName( + $table, + $tableElementData['column'], + $tableElementData['indexType'] ?? null + ); + $declaredStructure[$elementType][$indexName] = true; + } } } $elementType = 'constraint'; if (!empty($tableData[$elementType])) { foreach ($tableData[$elementType] as $tableElementData) { - if ($tableElementData['type'] === 'foreign') { - $referenceTable = $schema->getTableByName($tableElementData['referenceTable']); - $column = $table->getColumnByName($tableElementData['column']); - $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']); - $constraintName = ($column !== false && $referenceColumn !== false) ? - $this->elementNameResolver->getFullFKName( + $constraintName = null; + if (isset($tableElementData['type'], $tableElementData['column'])) { + if ($tableElementData['type'] === 'foreign') { + $column = $table->getColumnByName($tableElementData['column']); + $referenceTable = $schema->getTableByName($tableElementData['referenceTable'] ?? null); + $referenceColumn = ($referenceTable !== false) + ? $referenceTable->getColumnByName($tableElementData['referenceColumn'] ?? null) : false; + + $constraintName = ($column !== false && $referenceColumn !== false) ? + $this->elementNameResolver->getFullFKName( + $table, + $column, + $referenceTable, + $referenceColumn + ) : null; + } else { + $constraintName = $this->elementNameResolver->getFullIndexName( $table, - $column, - $referenceTable, - $referenceColumn - ) : null; - } else { - $constraintName = $this->elementNameResolver->getFullIndexName( - $table, - $tableElementData['column'], - $tableElementData['type'] - ); + $tableElementData['column'], + $tableElementData['type'] + ); + } } if ($constraintName) { $declaredStructure[$elementType][$constraintName] = true; diff --git a/app/code/Magento/Developer/README.md b/app/code/Magento/Developer/README.md index d5a6a2cee9d46..aa29586df140d 100644 --- a/app/code/Magento/Developer/README.md +++ b/app/code/Magento/Developer/README.md @@ -8,4 +8,4 @@ Extension developers can interact with the Magento_Developer module. For more in [The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Developer module. -A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. \ No newline at end of file +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index e64c7f1ae377b..76d91f924fd39 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -6,28 +6,61 @@ namespace Magento\Dhl\Model; +use Exception; use Laminas\Http\Request as HttpRequest; use Magento\Catalog\Model\Product\Type; +use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\Dhl\Model\Validator\XmlValidator; +use Magento\Directory\Helper\Data; +use Magento\Directory\Model\CountryFactory; +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Directory\Model\RegionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Async\CallbackDeferred; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\Framework\HTTP\AsyncClient\HttpException; use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface; use Magento\Framework\HTTP\AsyncClient\Request; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Framework\HTTP\LaminasClient; +use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Math\Division; use Magento\Framework\Measure\Length; use Magento\Framework\Measure\Weight; +use Magento\Framework\Model\AbstractModel; use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\StringUtils; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Quote\Model\Quote\Address\RateResult\Method; +use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Shipment; use Magento\Shipping\Model\Carrier\AbstractCarrier; +use Magento\Shipping\Model\Carrier\CarrierInterface; use Magento\Shipping\Model\Rate\Result; use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; +use Magento\Shipping\Model\Shipment\Request as ShipmentRequest; +use Magento\Shipping\Model\Simplexml\Element; +use Magento\Shipping\Model\Simplexml\ElementFactory; +use Magento\Shipping\Model\Tracking\Result\ErrorFactory; +use Magento\Shipping\Model\Tracking\Result\StatusFactory; +use Magento\Shipping\Model\Tracking\ResultFactory; +use Magento\Store\Model\Information; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; +use SimpleXMLElement; +use Throwable; +use const DATE_RFC3339; /** * DHL International (API v1.4) @@ -35,7 +68,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shipping\Model\Carrier\CarrierInterface +class Carrier extends AbstractDhl implements CarrierInterface { /**#@+ * Carrier Product indicator @@ -92,7 +125,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin /** * Countries parameters data * - * @var \Magento\Shipping\Model\Simplexml\Element|null + * @var Element|null */ protected $_countryParams; @@ -165,7 +198,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin /** * Core string * - * @var \Magento\Framework\Stdlib\StringUtils + * @var StringUtils */ protected $string; @@ -180,32 +213,32 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin protected $_coreDate; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Module\Dir\Reader + * @var Reader */ protected $_configReader; /** - * @var \Magento\Framework\Math\Division + * @var Division */ protected $mathDivision; /** - * @var \Magento\Framework\Filesystem\Directory\ReadFactory + * @var ReadFactory */ protected $readFactory; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $_dateTime; /** - * @var \Magento\Framework\HTTP\LaminasClientFactory + * @var LaminasClientFactory * phpcs:ignore Magento2.Commenting.ClassAndInterfacePHPDocFormatting * @deprecated Use asynchronous client. * @see $httpClient @@ -222,7 +255,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin /** * Xml response validator * - * @var \Magento\Dhl\Model\Validator\XmlValidator + * @var XmlValidator */ private $xmlValidator; @@ -242,64 +275,64 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin private $proxyDeferredFactory; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger * @param Security $xmlSecurity - * @param \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory + * @param ElementFactory $xmlElFactory * @param \Magento\Shipping\Model\Rate\ResultFactory $rateFactory - * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory - * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory - * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory - * @param \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory - * @param \Magento\Directory\Model\RegionFactory $regionFactory - * @param \Magento\Directory\Model\CountryFactory $countryFactory - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Directory\Helper\Data $directoryData - * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param MethodFactory $rateMethodFactory + * @param ResultFactory $trackFactory + * @param ErrorFactory $trackErrorFactory + * @param StatusFactory $trackStatusFactory + * @param RegionFactory $regionFactory + * @param CountryFactory $countryFactory + * @param CurrencyFactory $currencyFactory + * @param Data $directoryData + * @param StockRegistryInterface $stockRegistry * @param \Magento\Shipping\Helper\Carrier $carrierHelper * @param \Magento\Framework\Stdlib\DateTime\DateTime $coreDate - * @param \Magento\Framework\Module\Dir\Reader $configReader - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Stdlib\StringUtils $string - * @param \Magento\Framework\Math\Division $mathDivision - * @param \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory + * @param Reader $configReader + * @param StoreManagerInterface $storeManager + * @param StringUtils $string + * @param Division $mathDivision + * @param ReadFactory $readFactory + * @param DateTime $dateTime + * @param LaminasClientFactory $httpClientFactory * @param array $data - * @param \Magento\Dhl\Model\Validator\XmlValidator|null $xmlValidator + * @param XmlValidator|null $xmlValidator * @param ProductMetadataInterface|null $productMetadata * @param AsyncClientInterface|null $httpClient * @param ProxyDeferredFactory|null $proxyDeferredFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + ScopeConfigInterface $scopeConfig, \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory, - \Psr\Log\LoggerInterface $logger, + LoggerInterface $logger, Security $xmlSecurity, - \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory, + ElementFactory $xmlElFactory, \Magento\Shipping\Model\Rate\ResultFactory $rateFactory, - \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory, - \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory, - \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory, - \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory, - \Magento\Directory\Model\RegionFactory $regionFactory, - \Magento\Directory\Model\CountryFactory $countryFactory, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Directory\Helper\Data $directoryData, - \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + MethodFactory $rateMethodFactory, + ResultFactory $trackFactory, + ErrorFactory $trackErrorFactory, + StatusFactory $trackStatusFactory, + RegionFactory $regionFactory, + CountryFactory $countryFactory, + CurrencyFactory $currencyFactory, + Data $directoryData, + StockRegistryInterface $stockRegistry, \Magento\Shipping\Helper\Carrier $carrierHelper, \Magento\Framework\Stdlib\DateTime\DateTime $coreDate, - \Magento\Framework\Module\Dir\Reader $configReader, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Stdlib\StringUtils $string, - \Magento\Framework\Math\Division $mathDivision, - \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory, + Reader $configReader, + StoreManagerInterface $storeManager, + StringUtils $string, + Division $mathDivision, + ReadFactory $readFactory, + DateTime $dateTime, + LaminasClientFactory $httpClientFactory, array $data = [], - \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null, + XmlValidator $xmlValidator = null, ProductMetadataInterface $productMetadata = null, ?AsyncClientInterface $httpClient = null, ?ProxyDeferredFactory $proxyDeferredFactory = null @@ -353,7 +386,7 @@ protected function _getDefaultValue($origValue, $pathToValue) if (!$origValue) { $origValue = $this->_scopeConfig->getValue( $pathToValue, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->getStore() ); } @@ -377,7 +410,7 @@ public function collectRates(RateRequest $request) $this->setStore($requestDhl->getStoreId()); $origCompanyName = $this->_getDefaultValue( $requestDhl->getOrigCompanyName(), - \Magento\Store\Model\Information::XML_PATH_STORE_INFO_NAME + Information::XML_PATH_STORE_INFO_NAME ); $origCountryId = $this->_getDefaultValue($requestDhl->getOrigCountryId(), Shipment::XML_PATH_STORE_COUNTRY_ID); $origState = $this->_getDefaultValue($requestDhl->getOrigState(), Shipment::XML_PATH_STORE_REGION_ID); @@ -434,10 +467,10 @@ public function getResult() /** * Fills request object with Dhl config parameters * - * @param \Magento\Framework\DataObject $requestObject - * @return \Magento\Framework\DataObject + * @param DataObject $requestObject + * @return DataObject */ - protected function _addParams(\Magento\Framework\DataObject $requestObject) + protected function _addParams(DataObject $requestObject) { foreach ($this->_requestVariables as $code => $objectCode) { if ($this->_request->getDhlId()) { @@ -454,17 +487,17 @@ protected function _addParams(\Magento\Framework\DataObject $requestObject) /** * Prepare and set request in property of current instance * - * @param \Magento\Framework\DataObject $request + * @param DataObject $request * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function setRequest(\Magento\Framework\DataObject $request) + public function setRequest(DataObject $request) { $this->_request = $request; $this->setStore($request->getStoreId()); - $requestObject = new \Magento\Framework\DataObject(); + $requestObject = new DataObject(); $requestObject->setIsGenerateLabelReturn($request->getIsGenerateLabelReturn()); @@ -502,7 +535,7 @@ public function setRequest(\Magento\Framework\DataObject $request) ->setOrigEmail( $this->_scopeConfig->getValue( 'trans_email/ident_general/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $requestObject->getStoreId() ) ) @@ -515,7 +548,7 @@ public function setRequest(\Magento\Framework\DataObject $request) $originStreet2 = $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_ADDRESS2, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $requestObject->getStoreId() ); @@ -562,7 +595,7 @@ public function setRequest(\Magento\Framework\DataObject $request) * Get allowed shipping methods * * @return string[] - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getAllowedMethods() { @@ -582,7 +615,7 @@ public function getAllowedMethods() $allowedMethods = explode(',', $this->getConfigData('nondoc_methods') ?? ''); break; default: - throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Content Type')); + throw new LocalizedException(__('Wrong Content Type')); } } $methods = []; @@ -842,11 +875,11 @@ protected function _getAllItems() /** * Make pieces * - * @param \Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails + * @param Element $nodeBkgDetails * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _makePieces(\Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails) + protected function _makePieces(Element $nodeBkgDetails) { $divideOrderWeight = (string)$this->getConfigData('divide_order_weight'); $nodePieces = $nodeBkgDetails->addChild('Pieces', '', ''); @@ -951,7 +984,7 @@ protected function _getDimension($dimension, $configWeightUnit = false) /** * Add dimension to piece * - * @param \Magento\Shipping\Model\Simplexml\Element $nodePiece + * @param Element $nodePiece * @return void */ protected function _addDimension($nodePiece) @@ -1003,7 +1036,7 @@ function (array $a, array $b): int { ); $unavailable = true; } - } catch (\Throwable $exception) { + } catch (Throwable $exception) { //Failed to read response $unavailable = true; $this->_errors[$exception->getCode()] = $exception->getMessage(); @@ -1026,7 +1059,7 @@ function (array $a, array $b): int { /** * Get shipping quotes * - * @return \Magento\Framework\Model\AbstractModel|Result + * @return AbstractModel|Result */ protected function _getQuotes() { @@ -1047,7 +1080,7 @@ protected function _getQuotes() (string)$this->getConfigData('gateway_url'), Request::METHOD_POST, ['Content-Type' => 'application/xml'], - utf8_encode($request) + mb_convert_encoding($request, 'UTF-8') ) ), 'date' => $date, @@ -1105,7 +1138,7 @@ protected function _getQuotesFromServer($request) $client = $this->_httpClientFactory->create(); $client->setUri($this->getGatewayURL()); $client->setOptions(['maxredirects' => 0, 'timeout' => 30]); - $client->setRawBody(utf8_encode($request)); + $client->setRawBody(mb_convert_encoding($request, 'UTF-8')); $client->setMethod(HttpRequest::METHOD_POST); return $client->send()->getBody(); @@ -1114,7 +1147,7 @@ protected function _getQuotesFromServer($request) /** * Build quotes request XML object * - * @return \SimpleXMLElement + * @return SimpleXMLElement */ protected function _buildQuotesRequestXml() { @@ -1152,7 +1185,7 @@ protected function _buildQuotesRequestXml() $nodeBkgDetails->addChild('PaymentCountryCode', $rawRequest->getOrigCountryId()); $nodeBkgDetails->addChild( 'Date', - (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) + (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT) ); $nodeBkgDetails->addChild('ReadyTime', 'PT' . (int)(string)$this->getConfigData('ready_time') . 'H00M'); @@ -1185,11 +1218,11 @@ protected function _buildQuotesRequestXml() /** * Set pick-up date in request XML object * - * @param \SimpleXMLElement $requestXml + * @param SimpleXMLElement $requestXml * @param string $date - * @return \SimpleXMLElement + * @return SimpleXMLElement */ - protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date) + protected function _setQuotesRequestXmlDate(SimpleXMLElement $requestXml, $date) { $requestXml->GetQuote->BkgDetails->Date = $date; @@ -1200,8 +1233,8 @@ protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date * Parse response from DHL web service * * @param string $response - * @return bool|\Magento\Framework\DataObject|Result|Error - * @throws \Magento\Framework\Exception\LocalizedException + * @return bool|DataObject|Result|Error + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _parseResponse($response) @@ -1232,7 +1265,7 @@ protected function _parseResponse($response) foreach ($this->_rates as $rate) { $method = $rate['service']; $data = $rate['data']; - /* @var $rate \Magento\Quote\Model\Quote\Address\RateResult\Method */ + /* @var $rate Method */ $rate = $this->_rateMethodFactory->create(); $rate->setCarrier(self::CODE); $rate->setCarrierTitle($this->getConfigData('title')); @@ -1245,7 +1278,7 @@ protected function _parseResponse($response) } else { if (!empty($this->_errors)) { if ($this->_isShippingLabelFlag) { - throw new \Magento\Framework\Exception\LocalizedException($responseError); + throw new LocalizedException($responseError); } $this->debugErrors($this->_errors); } @@ -1258,11 +1291,11 @@ protected function _parseResponse($response) /** * Add rate to DHL rates array * - * @param \SimpleXMLElement $shipmentDetails + * @param SimpleXMLElement $shipmentDetails * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _addRate(\SimpleXMLElement $shipmentDetails) + protected function _addRate(SimpleXMLElement $shipmentDetails) { if (isset($shipmentDetails->ProductShortName) && isset($shipmentDetails->ShippingCharge) @@ -1279,7 +1312,7 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails) $dhlProductDescription = $this->getDhlProductTitle($dhlProduct); if ($currencyCode != $baseCurrencyCode) { - /* @var $currency \Magento\Directory\Model\Currency */ + /* @var $currency Currency */ $currency = $this->_currencyFactory->create(); $rates = $currency->getCurrencyRates($currencyCode, [$baseCurrencyCode]); if (!empty($rates) && isset($rates[$baseCurrencyCode])) { @@ -1334,14 +1367,14 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails) * Returns dimension unit (cm or inch) * * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _getDimensionUnit() { $countryId = $this->_rawRequest->getOrigCountryId(); $measureUnit = $this->getCountryParams($countryId)->getMeasureUnit(); if (empty($measureUnit)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Cannot identify measure unit for %1", $countryId) ); } @@ -1353,14 +1386,14 @@ protected function _getDimensionUnit() * Returns weight unit (kg or pound) * * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _getWeightUnit() { $countryId = $this->_rawRequest->getOrigCountryId(); $weightUnit = $this->getCountryParams($countryId)->getWeightUnit(); if (empty($weightUnit)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Cannot identify weight unit for %1", $countryId) ); } @@ -1372,7 +1405,7 @@ protected function _getWeightUnit() * Get Country Params by Country Code * * @param string $countryCode - * @return \Magento\Framework\DataObject + * @return DataObject * * @see $countryCode ISO 3166 Codes (Countries) A2 */ @@ -1385,19 +1418,20 @@ protected function getCountryParams($countryCode) $this->_countryParams = $this->_xmlElFactory->create(['data' => $countriesXml]); } if (isset($this->_countryParams->{$countryCode})) { - $countryParams = new \Magento\Framework\DataObject($this->_countryParams->{$countryCode}->asArray()); + $countryParams = new DataObject($this->_countryParams->{$countryCode}->asArray()); } - return $countryParams ?? new \Magento\Framework\DataObject(); + return $countryParams ?? new DataObject(); } /** * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response * - * @param \Magento\Framework\DataObject $request - * @return \Magento\Framework\DataObject + * @param DataObject $request + * @return DataObject */ - protected function _doShipmentRequest(\Magento\Framework\DataObject $request) + protected function _doShipmentRequest(DataObject $request) { + $this->_prepareShipmentRequest($request); $this->_mapRequestToShipment($request); $this->setRequest($request); @@ -1408,13 +1442,13 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) /** * Processing additional validation to check is carrier applicable. * - * @param \Magento\Framework\DataObject $request - * @return $this|\Magento\Framework\DataObject|boolean + * @param DataObject $request + * @return $this|DataObject|boolean * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 100.2.3 * @see use processAdditionalValidation method instead */ - public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + public function proccessAdditionalValidation(DataObject $request) { return $this->processAdditionalValidation($request); } @@ -1422,10 +1456,10 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ /** * Processing additional validation to check is carrier applicable. * - * @param \Magento\Framework\DataObject $request - * @return $this|\Magento\Framework\DataObject|boolean + * @param DataObject $request + * @return $this|DataObject|boolean */ - public function processAdditionalValidation(\Magento\Framework\DataObject $request) + public function processAdditionalValidation(DataObject $request) { //Skip by item validation if there is no items in request if (empty($this->getAllItems($request))) { @@ -1435,7 +1469,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque $countryParams = $this->getCountryParams( $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $request->getStoreId() ) ); @@ -1455,11 +1489,11 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque /** * Return container types of carrier * - * @param \Magento\Framework\DataObject|null $params + * @param DataObject|null $params * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function getContainerTypes(\Magento\Framework\DataObject $params = null) + public function getContainerTypes(DataObject $params = null) { return [ self::DHL_CONTENT_TYPE_DOC => __('Documents'), @@ -1470,11 +1504,11 @@ public function getContainerTypes(\Magento\Framework\DataObject $params = null) /** * Map request to shipment * - * @param \Magento\Framework\DataObject $request + * @param DataObject $request * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ - protected function _mapRequestToShipment(\Magento\Framework\DataObject $request) + protected function _mapRequestToShipment(DataObject $request) { $request->setOrigCountryId($request->getShipperAddressCountryCode()); $this->setRawRequest($request); @@ -1487,7 +1521,7 @@ protected function _mapRequestToShipment(\Magento\Framework\DataObject $request) $minValue = $this->_getMinDimension($params['dimension_units']); if ($params['width'] < $minValue || $params['length'] < $minValue || $params['height'] < $minValue) { $message = __('Height, width and length should be equal or greater than %1', $minValue); - throw new \Magento\Framework\Exception\LocalizedException($message); + throw new LocalizedException($message); } } @@ -1525,8 +1559,8 @@ protected function _getMinDimension($dimensionUnit) /** * Do rate request and handle errors * - * @return Result|\Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @return Result|DataObject + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -1562,7 +1596,7 @@ protected function _doRequest() $originRegion = $this->getCountryParams( $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->getStore() ) )->getRegion(); @@ -1658,8 +1692,10 @@ protected function _doRequest() $baseCurrencyCode = $this->_storeManager->getWebsite($rawRequest->getWebsiteId())->getBaseCurrencyCode(); $nodeDutiable->addChild('DeclaredCurrency', $baseCurrencyCode); $nodeDutiable->addChild('TermsOfTrade', 'DAP'); - } + /** Export Declaration */ + $this->addExportDeclaration($xml, $rawRequest); + } /** * Reference * This element identifies the reference information. It is an optional field in the @@ -1716,7 +1752,7 @@ protected function _doRequest() $request = $xml->asXML(); if ($request && !(mb_detect_encoding($request) == 'UTF-8')) { - $request = utf8_encode($request); + $request = mb_convert_encoding($request, 'UTF-8'); } $responseBody = $this->_getCachedQuotes($request); @@ -1731,10 +1767,10 @@ protected function _doRequest() $request ) ); - $responseBody = utf8_decode($response->get()->getBody()); + $responseBody = mb_convert_encoding($response->get()->getBody(), 'ISO-8859-1', 'UTF-8'); $debugData['result'] = $this->filterDebugData($responseBody); $this->_setCachedQuotes($request, $responseBody); - } catch (\Exception $e) { + } catch (Exception $e) { $this->_errors[$e->getCode()] = $e->getMessage(); $responseBody = ''; } @@ -1747,7 +1783,7 @@ protected function _doRequest() /** * Generation Shipment Details Node according to origin region * - * @param \Magento\Shipping\Model\Simplexml\Element $xml + * @param Element $xml * @param RateRequest $rawRequest * @param string $originRegion * @return void @@ -1880,7 +1916,7 @@ protected function _getXMLTracking($trackings) //$xml->addChild('PiecesEnabled', 'ALL_CHECK_POINTS'); $request = $xml->asXML(); - $request = utf8_encode($request); + $request = mb_convert_encoding($request, 'UTF-8'); $responseBody = $this->_getCachedQuotes($request); if ($responseBody === null) { @@ -1897,7 +1933,7 @@ protected function _getXMLTracking($trackings) $responseBody = $response->get()->getBody(); $debugData['result'] = $this->filterDebugData($responseBody); $this->_setCachedQuotes($request, $responseBody); - } catch (\Exception $e) { + } catch (Exception $e) { $this->_errors[$e->getCode()] = $e->getMessage(); $responseBody = ''; } @@ -1922,7 +1958,7 @@ protected function _parseXmlTrackingResponse($trackings, $response) $resultArr = []; if (!empty(trim($response))) { - $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class); + $xml = $this->parseXml($response, Element::class); if (!is_object($xml)) { $errorTitle = __('Response is in the wrong format'); } @@ -2021,19 +2057,19 @@ protected function _getPerpackagePrice($cost, $handlingType, $handlingFee) /** * Do request to shipment * - * @param \Magento\Shipping\Model\Shipment\Request $request - * @return array|\Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @param ShipmentRequest $request + * @return array|DataObject + * @throws LocalizedException */ public function requestToShipment($request) { $packages = $request->getPackages(); if (!is_array($packages) || !$packages) { - throw new \Magento\Framework\Exception\LocalizedException(__('No packages for request')); + throw new LocalizedException(__('No packages for request')); } $result = $this->_doShipmentRequest($request); - $response = new \Magento\Framework\DataObject( + $response = new DataObject( [ 'info' => [ [ @@ -2078,23 +2114,23 @@ protected function _checkDomesticStatus($origCountryCode, $destCountryCode) /** * Prepare shipping label data * - * @param \SimpleXMLElement $xml - * @return \Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @param SimpleXMLElement $xml + * @return DataObject + * @throws LocalizedException */ - protected function _prepareShippingLabelContent(\SimpleXMLElement $xml) + protected function _prepareShippingLabelContent(SimpleXMLElement $xml) { - $result = new \Magento\Framework\DataObject(); + $result = new DataObject(); try { if (!isset($xml->AirwayBillNumber) || !isset($xml->LabelImage->OutputImage)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Unable to retrieve shipping label')); + throw new LocalizedException(__('Unable to retrieve shipping label')); } $result->setTrackingNumber((string)$xml->AirwayBillNumber); $labelContent = (string)$xml->LabelImage->OutputImage; // phpcs:ignore Magento2.Functions.DiscouragedFunction $result->setShippingLabelContent(base64_decode($labelContent)); - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage())); + } catch (Exception $e) { + throw new LocalizedException(__($e->getMessage())); } return $result; @@ -2123,7 +2159,7 @@ protected function isDutiable($origCountryId, $destCountryId): bool */ private function buildMessageTimestamp(string $datetime = null): string { - return $this->_coreDate->date(\DATE_RFC3339, $datetime); + return $this->_coreDate->date(DATE_RFC3339, $datetime); } /** @@ -2131,7 +2167,7 @@ private function buildMessageTimestamp(string $datetime = null): string * * @param string $servicePrefix * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function buildMessageReference(string $servicePrefix): string { @@ -2142,7 +2178,7 @@ private function buildMessageReference(string $servicePrefix): string ]; if (!in_array($servicePrefix, $validPrefixes)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference") ); } @@ -2183,4 +2219,65 @@ private function getGatewayURL(): string return (string)$this->getConfigData('gateway_url'); } } + + /** + * Generating Export Declaration Details + * + * @param Element $xml + * @param ShipmentRequest $rawRequest + * @return void + */ + private function addExportDeclaration(Element $xml, ShipmentRequest $rawRequest): void + { + $nodeExportDeclaration = $xml->addChild('ExportDeclaration', '', ''); + $nodeExportDeclaration->addChild( + 'InvoiceNumber', + $rawRequest->getOrderShipment()->getOrder()->hasInvoices() + ? $this->getInvoiceNumbers($rawRequest) + : $rawRequest->getOrderShipment()->getOrder()->getIncrementId() + ); + $nodeExportDeclaration->addChild( + 'InvoiceDate', + date("Y-m-d", strtotime((string)$rawRequest->getOrderShipment()->getOrder()->getCreatedAt())) + ); + $exportItems = $rawRequest->getPackages(); + foreach ($exportItems as $exportItem) { + $itemWeightUnit = $exportItem['params']['weight_units'] ? substr( + $exportItem['params']['weight_units'], + 0, + 1 + ) : 'L'; + foreach ($exportItem['items'] as $itemNo => $itemData) { + $nodeExportItem = $nodeExportDeclaration->addChild('ExportLineItem', '', ''); + $nodeExportItem->addChild('LineNumber', $itemNo); + $nodeExportItem->addChild('Quantity', $itemData['qty']); + $nodeExportItem->addChild('QuantityUnit', 'PCS'); + $nodeExportItem->addChild('Description', $itemData['name']); + $nodeExportItem->addChild('Value', $itemData['price']); + $nodeItemWeight = $nodeExportItem->addChild('Weight', '', ''); + $nodeItemWeight->addChild('Weight', $itemData['weight']); + $nodeItemWeight->addChild('WeightUnit', $itemWeightUnit); + $nodeItemGrossWeight = $nodeExportItem->addChild('GrossWeight'); + $nodeItemGrossWeight->addChild('Weight', $itemData['weight']); + $nodeItemGrossWeight->addChild('WeightUnit', $itemWeightUnit); + $nodeExportItem->addChild('ManufactureCountryCode', $rawRequest->getShipperAddressCountryCode()); + } + } + } + + /** + * Fetching Shipment Order Invoice No + * + * @param ShipmentRequest $rawRequest + * @return string + */ + private function getInvoiceNumbers(ShipmentRequest $rawRequest): string + { + $invoiceNumbers = []; + $order = $rawRequest->getOrderShipment()->getOrder(); + foreach ($order->getInvoiceCollection() as $invoice) { + $invoiceNumbers[] = $invoice->getIncrementId(); + } + return implode(',', $invoiceNumbers); + } } diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 8473d0ae426ee..d0ad6f15705da 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -16,6 +16,7 @@ use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Framework\Json\Helper\Data as JsonData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; @@ -26,7 +27,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Data extends AbstractHelper +class Data extends AbstractHelper implements ResetAfterRequestInterface { private const STORE_ID = 'store_id'; @@ -435,4 +436,13 @@ private function getCurrentScope(): array return $scope; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_regionJson = null; + $this->_currencyCache = []; + } } diff --git a/app/code/Magento/Directory/Model/Country.php b/app/code/Magento/Directory/Model/Country.php index bc18e9bbd9531..ff8d4514d225d 100644 --- a/app/code/Magento/Directory/Model/Country.php +++ b/app/code/Magento/Directory/Model/Country.php @@ -65,6 +65,8 @@ public function __construct( } /** + * Country model constructor + * * @return void */ protected function _construct() @@ -95,6 +97,8 @@ public function getRegions() } /** + * Get region collection with loaded data + * * @return \Magento\Directory\Model\ResourceModel\Region\Collection */ public function getLoadedRegionCollection() @@ -105,6 +109,8 @@ public function getLoadedRegionCollection() } /** + * Get region collection + * * @return \Magento\Directory\Model\ResourceModel\Region\Collection */ public function getRegionCollection() @@ -115,6 +121,8 @@ public function getRegionCollection() } /** + * Format address + * * @param \Magento\Framework\DataObject $address * @param bool $html * @return string @@ -175,6 +183,14 @@ public function getFormats() return null; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_format = []; + } + /** * Retrieve country format * @@ -196,6 +212,7 @@ public function getFormat($type) /** * Get country name * + * @param mixed $locale * @return string */ public function getName($locale = null) diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index f84de7c3593fa..29499b0565a9a 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -6,16 +6,18 @@ namespace Magento\Directory\Model\ResourceModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Currency Resource Model * * @api * @since 100.0.2 */ -class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** - * Currency rate table + * Currency rate table name * * @var string */ @@ -233,4 +235,12 @@ protected function _getRatesByCode($code, $toCurrencies = null) return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_rateCache = []; + } } diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php new file mode 100644 index 0000000000000..23eb9eaa4c2f3 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php @@ -0,0 +1,103 @@ +<?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\Directory\Setup\DataInstallerFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +/** + * Add Costa Rica States/Regions + */ +class AddDataForCostaRica implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + 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->getDataForCostaRica() + ); + + return $this; + } + + /** + * Costa Rica states data.Pura Vida :) + * + * @return array + */ + private function getDataForCostaRica(): array + { + return [ + ['CR', 'CR-SJ', 'San José'], + ['CR', 'CR-AL', 'Alajuela'], + ['CR', 'CR-CA', 'Cartago'], + ['CR', 'CR-HE', 'Heredia'], + ['CR', 'CR-GU', 'Guanacaste'], + ['CR', 'CR-PU', 'Puntarenas'], + ['CR', 'CR-LI', 'Limón'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } + + /** + * Get version + * + * @return string + */ + public static function getVersion() + { + return '2.4.2'; + } +} diff --git a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js index 9d22cf90f258e..e6e08fddacda4 100644 --- a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js @@ -40,6 +40,10 @@ define([ $(this.options.regionListId).on('change', $.proxy(function (e) { this.setOption = false; this.currentRegionOption = $(e.target).val(); + + if (!this.currentRegionOption) { + $(this.options.regionListId).add(this.options.regionInputId).val(''); + } }, this)); $(this.options.regionInputId).on('focusout', $.proxy(function () { diff --git a/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php new file mode 100644 index 0000000000000..16ba9dcab769b --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config; + +use Magento\DirectoryGraphQl\Model\Resolver\Country\Identity; +use Magento\Framework\App\Config\ValueInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Config\Cache\Tag\Strategy\TagGeneratorInterface; + +/** + * Generator that generates cache tags for country configuration + */ +class CountryTagGenerator implements TagGeneratorInterface +{ + /** + * @var string[] + */ + private $countryConfigPaths = [ + 'general/locale/code', + 'general/country/allow' + ]; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function generateTags(ValueInterface $config): array + { + if (in_array($config->getPath(), $this->countryConfigPaths)) { + if ($config->getScope() == ScopeInterface::SCOPE_WEBSITES) { + $website = $this->storeManager->getWebsite($config->getScopeId()); + $storeIds = $website->getStoreIds(); + } elseif ($config->getScope() == ScopeInterface::SCOPE_STORES) { + $storeIds = [$config->getScopeId()]; + } else { + $storeIds = array_keys($this->storeManager->getStores()); + } + $tags = []; + foreach ($storeIds as $storeId) { + $tags[] = sprintf('%s_%s', Identity::CACHE_TAG, $storeId); + } + return $tags; + } + return []; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php new file mode 100644 index 0000000000000..bc905b57b622e --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver\Country; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Store\Model\StoreManagerInterface; + +class Identity implements IdentityInterface +{ + /** + * @var string + */ + public const CACHE_TAG = 'gql_country'; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData)) { + return []; + } + $storeId = $this->storeManager->getStore()->getId(); + return [self::CACHE_TAG, sprintf('%s_%s', self::CACHE_TAG, $storeId)]; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/etc/di.xml b/app/code/Magento/DirectoryGraphQl/etc/di.xml index 3513eeeed2443..19b8495c66b67 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/di.xml +++ b/app/code/Magento/DirectoryGraphQl/etc/di.xml @@ -12,6 +12,9 @@ <item name="currency_tag_generator" xsi:type="object"> Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CurrencyTagGenerator </item> + <item name="country_tag_generator" xsi:type="object"> + Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CountryTagGenerator + </item> </argument> </arguments> </type> diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls index 7b7f1b6eb47ac..b5176b88ee206 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -3,8 +3,8 @@ type Query { currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "Return information about the store's currency.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency\\Identity") - countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheable: false) - country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheable: false) + countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity") + country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity") } type Currency { diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml index 1760f2228bf05..ce2f7dd577efe 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3247"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml index bfa0c77280f42..9e3cb81d42584 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-10929"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="downloadable"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml index bfa53f9beb4f8..089878f7e7a0a 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!-- Open Dropdown and select downloadable product option --> <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection" after="waitForSimpleProductPageLoad"/> diff --git a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php index 8ded865057dc7..10e7ddbb86c22 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php @@ -9,11 +9,14 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Downloadable\Api\Data\LinkInterface; use Magento\Downloadable\Helper\File as DownloadableFile; use Magento\Downloadable\Model\Link as LinkModel; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Data\Links; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\UrlInterface; @@ -78,11 +81,14 @@ protected function setUp(): void { $this->objectManagerHelper = new ObjectManagerHelper($this); $this->productMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getLinksTitle', 'getId', 'getTypeId']) + ->onlyMethods(['getId', 'getTypeId']) + ->addMethods(['getLinksTitle', 'getTypeInstance', 'getStoreId']) ->getMockForAbstractClass(); $this->locatorMock = $this->getMockForAbstractClass(LocatorInterface::class); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $this->escaperMock = $this->createMock(Escaper::class); + $this->escaperMock = $this->getMockBuilder(Escaper::class) + ->onlyMethods(['escapeHtml']) + ->getMockForAbstractClass(); $this->downloadableFileMock = $this->createMock(DownloadableFile::class); $this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); $this->linkModelMock = $this->createMock(LinkModel::class); @@ -100,6 +106,8 @@ protected function setUp(): void } /** + * Test case for getLinksTitle + * * @param int|null $id * @param string $typeId * @param InvokedCount $expectedGetTitle @@ -161,4 +169,183 @@ public function getLinksTitleDataProvider() ], ]; } + + /** + * Test case for getLinksData + * + * @param $productTypeMock + * @param string $typeId + * @param int $storeId + * @param array $links + * @param array $expectedLinksData + * @return void + * @dataProvider getLinksDataProvider + */ + public function testGetLinksData( + $productTypeMock, + string $typeId, + int $storeId, + array $links, + array $expectedLinksData + ): void { + $this->locatorMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + if (!empty($expectedLinksData)) { + $this->escaperMock->expects($this->any()) + ->method('escapeHtml') + ->willReturn($expectedLinksData['title']); + } + $this->productMock->expects($this->any()) + ->method('getTypeId') + ->willReturn($typeId); + $this->productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($productTypeMock); + $this->productMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $productTypeMock->expects($this->any()) + ->method('getLinks') + ->willReturn($links); + $getLinksData = $this->links->getLinksData(); + if (!empty($getLinksData)) { + $actualResult = current($getLinksData); + } else { + $actualResult = $getLinksData; + } + $this->assertEquals($expectedLinksData, $actualResult); + } + + /** + * Get Links data provider + * + * @return array + */ + public function getLinksDataProvider() + { + $productData1 = [ + 'link_id' => '1', + 'title' => 'test', + 'price' => '0.00', + 'number_of_downloads' => '0', + 'is_shareable' => '1', + 'link_url' => 'http://cdn.sourcebooks.com/test', + 'type' => 'url', + 'sample' => + [ + 'url' => null, + 'type' => null, + ], + 'sort_order' => '1', + 'is_unlimited' => '1', + 'use_default_price' => '0', + 'use_default_title' => '0', + + ]; + $productData2 = $productData1; + unset($productData2['use_default_price']); + unset($productData2['use_default_title']); + $productData3 = [ + 'link_id' => '1', + 'title' => 'simple', + 'price' => '10.00', + 'number_of_downloads' => '0', + 'is_shareable' => '0', + 'link_url' => '', + 'type' => 'simple', + 'sample' => + [ + 'url' => null, + 'type' => null, + ], + 'sort_order' => '1', + 'is_unlimited' => '1', + 'use_default_price' => '0', + 'use_default_title' => '0', + + ]; + $linkMock1 = $this->getLinkMockObject($productData1, '1', '1'); + $linkMock2 = $this->getLinkMockObject($productData1, '0', '0'); + $linkMock3 = $this->getLinkMockObject($productData3, '0', '0'); + return [ + 'test case for downloadable product for default store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => Type::TYPE_DOWNLOADABLE, + 'store_id' => 1, + 'links' => [$linkMock1], + 'expectedLinksData' => $productData1 + ], + 'test case for downloadable product for all store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => Type::TYPE_DOWNLOADABLE, + 'store_id' => 0, + 'links' => [$linkMock2], + 'expectedLinksData' => $productData2 + ], + 'test case for simple product for default store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => ProductType::TYPE_SIMPLE, + 'store_id' => 1, + 'links' => [$linkMock3], + 'expectedLinksData' => [] + ], + ]; + } + + /** + * Data provider for getLinks + * + * @param array $productData + * @param string $useDefaultPrice + * @param string $useDefaultTitle + * @return MockObject + */ + private function getLinkMockObject( + array $productData, + string $useDefaultPrice, + string $useDefaultTitle + ): MockObject { + $linkMock = $this->getMockBuilder(LinkInterface::class) + ->onlyMethods(['getId']) + ->addMethods(['getWebsitePrice', 'getStoreTitle']) + ->getMockForAbstractClass(); + $linkMock->expects($this->any()) + ->method('getId') + ->willReturn($productData['link_id']); + $linkMock->expects($this->any()) + ->method('getTitle') + ->willReturn($productData['title']); + $linkMock->expects($this->any()) + ->method('getPrice') + ->willReturn($productData['price']); + $linkMock->expects($this->any()) + ->method('getNumberOfDownloads') + ->willReturn($productData['number_of_downloads']); + $linkMock->expects($this->any()) + ->method('getIsShareable') + ->willReturn($productData['is_shareable']); + $linkMock->expects($this->any()) + ->method('getLinkUrl') + ->willReturn($productData['link_url']); + $linkMock->expects($this->any()) + ->method('getLinkType') + ->willReturn($productData['type']); + $linkMock->expects($this->any()) + ->method('getSampleUrl') + ->willReturn($productData['sample']['url']); + $linkMock->expects($this->any()) + ->method('getSampleType') + ->willReturn($productData['sample']['type']); + $linkMock->expects($this->any()) + ->method('getSortOrder') + ->willReturn($productData['sort_order']); + $linkMock->expects($this->any()) + ->method('getWebsitePrice') + ->willReturn($useDefaultPrice); + $linkMock->expects($this->any()) + ->method('getStoreTitle') + ->willReturn($useDefaultTitle); + return $linkMock; + } } diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php index 3be1094f7a4b7..7c3c30482fd85 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php @@ -120,7 +120,7 @@ public function getLinksData() $linkData = []; $linkData['link_id'] = $link->getId(); $linkData['title'] = $this->escaper->escapeHtml($link->getTitle()); - $linkData['price'] = $this->getPriceValue($link->getPrice()); + $linkData['price'] = $this->getPriceValue((float) $link->getPrice()); $linkData['number_of_downloads'] = $link->getNumberOfDownloads(); $linkData['is_shareable'] = $link->getIsShareable(); $linkData['link_url'] = $link->getLinkUrl(); diff --git a/app/code/Magento/Eav/Model/Attribute.php b/app/code/Magento/Eav/Model/Attribute.php index 40f9a4ae4e934..7699577113211 100644 --- a/app/code/Magento/Eav/Model/Attribute.php +++ b/app/code/Magento/Eav/Model/Attribute.php @@ -3,15 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** - * EAV attribute resource model (Using Forms) - * - * @method \Magento\Eav\Model\Attribute\Data\AbstractData|null getDataModel() - * Get data model linked to attribute or null. - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Eav\Model; use Magento\Store\Model\Website; @@ -23,14 +16,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute { /** - * Name of the module - * Override it - */ - //const MODULE_NAME = 'Magento_Eav'; - - /** - * Name of the module - * Override it + * @var string */ protected $_eventObject = 'attribute'; @@ -80,7 +66,7 @@ public function afterSave() } /** - * Return forms in which the attribute + * Return forms in which the attribute is being used * * @return array */ @@ -110,6 +96,18 @@ public function getValidateRules() return []; } + /** + * @inheritdoc + */ + public function setData($key, $value = null): Attribute + { + if ($key === 'used_in_forms') { + $this->setOrigData('used_in_forms', $this->getData('used_in_forms') ?? []); + } + parent::setData($key, $value); + return $this; + } + /** * Set validate rules * @@ -188,7 +186,7 @@ public function getMultilineCount() } /** - * {@inheritdoc} + * @inheritdoc */ public function afterDelete() { diff --git a/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php new file mode 100644 index 0000000000000..9317426addadc --- /dev/null +++ b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\Cache; + +use Magento\Framework\Api\AttributeInterface; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Eav\Model\Entity\Attribute; + +/** + * Cache identity provider for attributes form query + */ +class AttributesFormIdentity implements IdentityInterface +{ + public const CACHE_TAG = 'EAV_FORM'; + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData['items'])) { + return []; + } + + $identities = []; + + if ($resolvedData['formCode'] !== '') { + $identities[] = sprintf( + "%s_%s_FORM", + self::CACHE_TAG, + $resolvedData['formCode'] ?? '' + ); + } + + foreach ($resolvedData['items'] as $item) { + if ($item['attribute'] instanceof AttributeInterface) { + $identities[] = sprintf( + "%s_%s", + Attribute::CACHE_TAG, + $item['attribute']->getAttributeId() + ); + } + } + return $identities; + } +} diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php index a7e49b126f350..ab4cb121fd166 100644 --- a/app/code/Magento/Eav/Model/Config.php +++ b/app/code/Magento/Eav/Model/Config.php @@ -13,6 +13,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\SerializerInterface; /** @@ -24,7 +25,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ -class Config +class Config implements ResetAfterRequestInterface { /**#@+ * EAV cache ids @@ -217,7 +218,7 @@ public function clear() $this->_cache->clean( [ \Magento\Eav\Model\Cache\Type::CACHE_TAG, - \Magento\Eav\Model\Entity\Attribute::CACHE_TAG, + \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ] ); return $this; @@ -402,7 +403,7 @@ protected function _initEntityTypes() $this->_entityTypeData[$typeCode] = $typeData; } - if ($this->isCacheEnabled()) { + if ($this->isCacheEnabled() && !empty($this->_entityTypeData)) { $this->_cache->save( $this->serializer->serialize($this->_entityTypeData), self::ENTITIES_CACHE_ID, @@ -981,4 +982,20 @@ private function getWebsiteId(Collection $attributeCollection): int { return $attributeCollection->getWebsite() ? (int)$attributeCollection->getWebsite()->getId() : 0; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributesPerSet = []; + $this->_attributeData = []; + foreach ($this->attributes ?? [] as $attributesGroupedByEntityTypeCode) { + foreach ($attributesGroupedByEntityTypeCode as $attribute) { + if ($attribute instanceof ResetAfterRequestInterface) { + $attribute->_resetState(); + } + } + } + } } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index f66ade9435813..23c74e5b8e80b 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -22,6 +22,7 @@ use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Entity/Attribute/Model - entity abstract @@ -34,7 +35,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -abstract class AbstractEntity extends AbstractResource implements EntityInterface, DefaultAttributesProvider +abstract class AbstractEntity extends AbstractResource implements + EntityInterface, + DefaultAttributesProvider, + ResetAfterRequestInterface { /** * @var \Magento\Eav\Model\Entity\AttributeLoaderInterface @@ -2021,4 +2025,18 @@ protected function loadAttributesForObject($attributes, $object = null) } } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_attributesByCode = []; + $this->attributesByScope = []; + $this->_attributesByTable = []; + $this->_staticAttributes = []; + $this->_attributeValuesToDelete = []; + $this->_attributeValuesToSave = []; + self::$_attributeBackendTables = []; + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index e1094a331149e..ecdb2f55f4f46 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity; use Magento\Eav\Model\ReservedAttributeCheckerInterface; @@ -12,6 +14,8 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Cache\AttributesFormIdentity; /** * EAV Entity attribute model @@ -522,7 +526,35 @@ public function getSortWeight($setId) */ public function getIdentities() { - return [self::CACHE_TAG . '_' . $this->getId()]; + $identities = [self::CACHE_TAG . '_' . $this->getId()]; + + if (($this->hasDataChanges() || $this->isDeleted())) { + $identities[] = sprintf( + "%s_%s_ENTITY", + Config::ENTITIES_CACHE_ID, + strtoupper($this->getEntityType()->getEntityTypeCode()) + ); + + $usedBeforeChange = $this->getOrigData('used_in_forms') ?? []; + $usedInForms = $this->getUsedInForms() ?? []; + + if (is_array($usedBeforeChange) && is_array($usedInForms) && ($usedBeforeChange != $usedInForms)) { + $formsToInvalidate = array_merge( + array_diff($usedBeforeChange, $usedInForms), + array_diff($usedInForms, $usedBeforeChange) + ); + + foreach ($formsToInvalidate as $form) { + $identities[] = sprintf( + "%s_%s_FORM", + AttributesFormIdentity::CACHE_TAG, + $form + ); + }; + } + } + + return $identities; } /** diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index af621e17f4249..5628787f7debb 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; /** @@ -23,14 +24,15 @@ */ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtensibleModel implements AttributeInterface, - \Magento\Eav\Api\Data\AttributeInterface + \Magento\Eav\Api\Data\AttributeInterface, + ResetAfterRequestInterface { - const TYPE_STATIC = 'static'; + public const TYPE_STATIC = 'static'; /** * Const for empty string value. */ - const EMPTY_STRING = ''; + public const EMPTY_STRING = ''; /** * Attribute name @@ -68,8 +70,6 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens protected $_source; /** - * Attribute id cache - * * @var array */ protected $_attributeIdCache = []; @@ -219,8 +219,6 @@ public function __construct( /** * Get Serializer instance. * - * @deprecated 101.0.0 - * * @return Json * @since 101.0.0 */ @@ -229,7 +227,6 @@ protected function getSerializer() if ($this->serializer === null) { $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()->create(Json::class); } - return $this->serializer; } @@ -930,6 +927,7 @@ public function _getFlatColumnsDdlDefinition() * Used in database compatible mode * * @deprecated 101.0.0 + * @see MMDB * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -1444,4 +1442,22 @@ public function __wakeup() $this->dataObjectProcessor = $objectManager->get(\Magento\Framework\Reflection\DataObjectProcessor::class); $this->dataObjectHelper = $objectManager->get(\Magento\Framework\Api\DataObjectHelper::class); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->unsetData('store_label'); // store specific + $this->unsetData(self::OPTIONS); // store specific + if ($this->_source instanceof ResetAfterRequestInterface) { + $this->_source->_resetState(); + } + if ($this->_backend instanceof ResetAfterRequestInterface) { + $this->_backend->_resetState(); + } + if ($this->_frontend instanceof ResetAfterRequestInterface) { + $this->_frontend->_resetState(); + } + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 6f6dc0a47f5ae..2c9b6d68b0bbf 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -139,6 +139,7 @@ private function saveOption( $options = []; $options['value'][$optionId][0] = $optionLabel; $options['order'][$optionId] = $option->getSortOrder(); + $options['is_default'][$optionId] = $option->getIsDefault(); if (is_array($option->getStoreLabels())) { foreach ($option->getStoreLabels() as $label) { $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index bdd2899b47b67..63c823e25affc 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -6,6 +6,7 @@ namespace Magento\Eav\Model\Entity\Attribute\Source; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -14,7 +15,7 @@ * @api * @since 100.0.2 */ -class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource +class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource implements ResetAfterRequestInterface { /** * Default values for option cache @@ -97,6 +98,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) * * @return StoreManagerInterface * @deprecated 100.1.6 + * @see we don't recommend this approach anymore */ private function getStoreManager() { @@ -293,4 +295,13 @@ public function getFlatUpdateSelect($store) { return $this->_attrOptionFactory->create()->getFlatUpdateSelect($this->getAttribute(), $store); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_optionsDefault = []; + $this->_options = []; + } } diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index abd817940956f..1bc2ca7f63dfa 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -19,6 +19,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @since 100.0.2 */ abstract class AbstractCollection extends AbstractDb implements SourceProviderInterface @@ -180,6 +181,23 @@ protected function _construct() { } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_itemsById = []; + $this->_staticFields = []; + $this->_entity = null; + $this->_selectEntityTypes = []; + $this->_selectAttributes = []; + $this->_filterAttributes = []; + $this->_joinEntities = []; + $this->_joinAttributes = []; + $this->_joinFields = []; + } + /** * Retrieve table name * @@ -1597,14 +1615,12 @@ protected function _afterLoad() protected function _reset() { parent::_reset(); - $this->_selectEntityTypes = []; $this->_selectAttributes = []; $this->_filterAttributes = []; $this->_joinEntities = []; $this->_joinAttributes = []; $this->_joinFields = []; - return $this; } diff --git a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php index b994d793ed04c..c25c7f5ec90ed 100644 --- a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php +++ b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php @@ -5,10 +5,13 @@ */ namespace Magento\Eav\Model\Entity\VersionControl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata as ResourceModelMetaData; + /** * Class Metadata represents a list of entity fields that are applicable for persistence operations */ -class Metadata extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata +class Metadata extends ResourceModelMetaData implements ResetAfterRequestInterface { /** * Returns list of entity fields that are applicable for persistence operations @@ -36,4 +39,12 @@ public function getFields(\Magento\Framework\DataObject $entity) return $this->metadataInfo[$entityClass]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->metadataInfo = []; + } } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php index 7abb54e780f5f..897640f852cc7 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php @@ -10,6 +10,7 @@ /** * EAV additional attribute resource collection (Using Forms) * + * phpcs:disable Magento2.Classes.AbstractApi.AbstractApi * @api * @since 100.0.2 */ @@ -18,7 +19,7 @@ abstract class Collection extends \Magento\Eav\Model\ResourceModel\Entity\Attrib /** * code of password hash in customer's EAV tables */ - const EAV_CODE_PASSWORD_HASH = 'password_hash'; + public const EAV_CODE_PASSWORD_HASH = 'password_hash'; /** * Current website scope instance @@ -64,6 +65,15 @@ public function __construct( parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $eavConfig, $connection, $resource); } + /** + * @inheritDoc + */ + public function _resetState(): void //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedFunction + { + /* Note: because Eav attribute loading takes significant performance, + we are not resetting it like other collections. */ + } + /** * Default attribute entity type code * @@ -212,6 +222,7 @@ protected function _initSelect() /** * Specify attribute entity type filter. + * * Entity type is defined. * * @param int $type diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php index 9c6adc0354f8d..1711d87c26487 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php +++ b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php @@ -6,18 +6,15 @@ namespace Magento\Eav\Model\ResourceModel; -use Magento\Catalog\Model\Product; use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\EntityManager\EntityMetadataInterface; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Locale\FormatInterface; use Magento\Framework\Model\Entity\ScopeInterface; -use Magento\Framework\EntityManager\MetadataPool; /** - * Class AttributePersistor + * Class AttributePersistor persists attributes */ class AttributePersistor { @@ -67,6 +64,8 @@ public function __construct( } /** + * Registers delete + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -78,6 +77,8 @@ public function registerDelete($entityType, $link, $attributeCode) } /** + * Registers update + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -90,6 +91,8 @@ public function registerUpdate($entityType, $link, $attributeCode, $value) } /** + * Registers Insert + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -102,6 +105,8 @@ public function registerInsert($entityType, $link, $attributeCode, $value) } /** + * Process deletes + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -132,6 +137,8 @@ public function processDeletes($entityType, $context) } /** + * Process inserts + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -194,6 +201,8 @@ private function prepareInsertDataForMultipleSave($entityType, $context) } /** + * Process updates + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -329,10 +338,14 @@ public function flush($entityType, $context) $this->processDeletes($entityType, $context); $this->processInserts($entityType, $context); $this->processUpdates($entityType, $context); - unset($this->delete, $this->insert, $this->update); + $this->delete = []; + $this->insert = []; + $this->update = []; } /** + * Prepares value + * * @param string $entityType * @param string $value * @param AbstractAttribute $attribute @@ -355,6 +368,8 @@ protected function prepareValue($entityType, $value, AbstractAttribute $attribut } /** + * Gets scope value + * * @param ScopeInterface $scope * @param AbstractAttribute $attribute * @param bool $useDefault diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 3e894c5f76a16..b11e88b4e1217 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -17,6 +17,7 @@ use Magento\Framework\DB\Select; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; @@ -53,6 +54,11 @@ class Attribute extends AbstractDb */ private $config; + /** + * @var PoisonPillPutInterface + */ + private $pillPut; + /** * Class constructor * @@ -60,16 +66,20 @@ class Attribute extends AbstractDb * @param StoreManagerInterface $storeManager * @param Type $eavEntityType * @param string $connectionName + * @param PoisonPillPutInterface|null $pillPut * @codeCoverageIgnore */ public function __construct( Context $context, StoreManagerInterface $storeManager, Type $eavEntityType, - $connectionName = null + $connectionName = null, + PoisonPillPutInterface $pillPut = null ) { $this->_storeManager = $storeManager; $this->_eavEntityType = $eavEntityType; + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PoisonPillPutInterface::class); parent::__construct($context, $connectionName); } @@ -235,6 +245,7 @@ protected function _afterSave(AbstractModel $object) $object ); $this->getConfig()->clear(); + $this->pillPut->put(); return parent::_afterSave($object); } @@ -249,6 +260,7 @@ protected function _afterSave(AbstractModel $object) protected function _afterDelete(AbstractModel $object) { $this->getConfig()->clear(); + $this->pillPut->put(); return $this; } @@ -256,7 +268,6 @@ protected function _afterDelete(AbstractModel $object) * Returns config instance * * @return Config - * @deprecated 100.0.7 */ private function getConfig() { @@ -388,6 +399,10 @@ protected function _saveOption(AbstractModel $object) $defaultValue = $this->_processAttributeOptions($object, $option); } + if ($object->getDefaultValue()) { + $defaultValue[] = $object->getDefaultValue(); + } + $this->_saveDefaultValue($object, $defaultValue); return $this; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php index 153735f988376..554896a704094 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php @@ -32,14 +32,14 @@ public function __construct(ResourceConnection $connection) /** * Get EAV attribute option value by option id * - * @param int $valueId + * @param int $optionId * @return string|null */ - public function get(int $valueId): ?string + public function get(int $optionId): ?string { $select = $this->connection->select() ->from($this->connection->getTableName('eav_attribute_option_value'), 'value') - ->where('value_id = ?', $valueId); + ->where('option_id = ?', $optionId); $result = $this->connection->fetchOne($select); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php index 9438178e56085..063b6041782d7 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php @@ -96,6 +96,16 @@ protected function _construct() } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_store = null; + $this->_entityType = null; + } + /** * Get EAV website table * @@ -193,6 +203,7 @@ public function setSortOrder($direction = self::SORT_ORDER_ASC) */ protected function _beforeLoad() { + $store = $this->getStore(); $select = $this->getSelect(); $connection = $this->getConnection(); $entityType = $this->getEntityType(); @@ -254,7 +265,6 @@ protected function _beforeLoad() } } - $store = $this->getStore(); $joinWebsiteExpression = $connection->quoteInto( 'sa.attribute_id = main_table.attribute_id AND sa.website_id = ?', (int)$store->getWebsiteId() diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index 7b29b9dde6790..4175608db5f08 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -8,6 +8,7 @@ use Magento\Eav\Model\Attribute; use Magento\Eav\Model\AttributeDataFactory; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; /** @@ -47,18 +48,39 @@ class Data extends \Magento\Framework\Validator\AbstractValidator */ private $ignoredAttributesByTypesList; + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * @param AttributeDataFactory $attrDataFactory + * @param Config|null $eavConfig * @param array $ignoredAttributesByTypesList */ public function __construct( AttributeDataFactory $attrDataFactory, + Config $eavConfig = null, array $ignoredAttributesByTypesList = [] ) { + $this->eavConfig = $eavConfig ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(Config::class); $this->_attrDataFactory = $attrDataFactory; $this->ignoredAttributesByTypesList = $ignoredAttributesByTypesList; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_attributes = []; + $this->allowedAttributesList = []; + $this->deniedAttributesList = []; + $this->_data = []; + } + /** * Set list of attributes for validation in isValid method. * @@ -166,8 +188,9 @@ protected function _getAttributes($entity) } elseif ($entity instanceof \Magento\Framework\Model\AbstractModel && $entity->getResource() instanceof \Magento\Eav\Model\Entity\AbstractEntity ) { // $entity is EAV-model + $type = $entity->getEntityType()->getEntityTypeCode(); /** @var \Magento\Eav\Model\Entity\Type $entityType */ - $entityType = $entity->getEntityType(); + $entityType = $this->eavConfig->getEntityType($type); $attributes = $entityType->getAttributeCollection()->getItems(); $ignoredTypeAttributes = $this->ignoredAttributesByTypesList[$entityType->getEntityTypeCode()] ?? []; diff --git a/app/code/Magento/Eav/README.md b/app/code/Magento/Eav/README.md index 6710044ac6c89..f669e534a973a 100644 --- a/app/code/Magento/Eav/README.md +++ b/app/code/Magento/Eav/README.md @@ -1,2 +1,2 @@ Magento\EAV stands for Entity-Attribute-Value. The purpose of Magento\Eav module is to make entities -configurable/extendable by admin user. \ No newline at end of file +configurable/extendable by admin user. diff --git a/app/code/Magento/Eav/Test/Fixture/Attribute.php b/app/code/Magento/Eav/Test/Fixture/Attribute.php new file mode 100644 index 0000000000000..cc7f66594d5c2 --- /dev/null +++ b/app/code/Magento/Eav/Test/Fixture/Attribute.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Fixture; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class Attribute implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'entity_type_id' => null, + 'attribute_id' => null, + 'attribute_code' => 'attribute%uniqid%', + 'default_frontend_label' => 'Attribute%uniqid%', + 'frontend_labels' => [], + 'frontend_input' => 'text', + 'backend_type' => 'varchar', + 'is_required' => false, + 'is_user_defined' => true, + 'note' => null, + 'backend_model' => null, + 'source_model' => null, + 'default_value' => null, + 'is_unique' => '0', + 'frontend_class' => null + ]; + + /** + * @var ServiceFactory + */ + private ServiceFactory $serviceFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param ServiceFactory $serviceFactory + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + */ + public function __construct( + ServiceFactory $serviceFactory, + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository + ) { + $this->serviceFactory = $serviceFactory; + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data['entity_type_id'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)); + + $this->serviceFactory->create(AttributeRepositoryInterface::class, 'save')->execute( + [ + 'attribute' => $mergedData + ] + ); + + return $this->attributeRepository->get($mergedData['entity_type_id'], $mergedData['attribute_code']); + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Eav/Test/Fixture/AttributeOption.php b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php new file mode 100644 index 0000000000000..4d25bdb03ca87 --- /dev/null +++ b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Fixture; + +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\DataFixtureInterface; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class AttributeOption implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'entity_type' => null, + 'attribute_code' => null, + 'label' => 'Option Label %uniqid%', + 'sort_order' => null, + 'store_labels' => '', + 'is_default' => false + ]; + + /** + * @var ServiceFactory + */ + private ServiceFactory $serviceFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param ServiceFactory $serviceFactory + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + */ + public function __construct( + ServiceFactory $serviceFactory, + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository + ) { + $this->serviceFactory = $serviceFactory; + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data['entity_type'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute option', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + if (empty($data['attribute_code'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute option', + [ + 'field' => 'attribute_code' + ] + ) + ); + } + + $mergedData = array_filter( + $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)), + function ($value) { + return $value !== null; + } + ); + + $entityType = $mergedData['entity_type']; + $attributeCode = $mergedData['attribute_code']; + unset($mergedData['entity_type'], $mergedData['attribute_code']); + + $this->serviceFactory->create(AttributeOptionManagementInterface::class, 'add')->execute( + [ + 'entityType' => $entityType, + 'attributeCode' => $attributeCode, + 'option' => $mergedData + ] + ); + + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + + foreach ($attribute->getOptions() as $option) { + if ($this->getDefaultLabel($mergedData) === $option->getLabel()) { + if (isset($mergedData['is_default']) && $mergedData['is_default']) { + $option->setIsDefault(true); + } + return $option; + } + } + + return null; + } + + /** + * Retrieve default label or label for default store + * + * @param array $mergedData + * @return string + */ + private function getDefaultLabel(array $mergedData): string + { + $defaultLabel = $mergedData['label']; + if (!isset($mergedData['store_labels']) || !is_array($mergedData['store_labels'])) { + return $defaultLabel; + } + + foreach ($mergedData['store_labels'] as $label) { + if (isset($label['store_id']) && $label['store_id'] === 0 && isset($label['label'])) { + return $label['label']; + } + } + + return $defaultLabel; + } +} diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index 7b554a19fc281..278bcfdaad440 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -80,6 +80,9 @@ public function testAdd(string $label): void ], 'order' => [ 'id_new_option' => $sortOder, + ], + 'is_default' => [ + 'id_new_option' => true, ] ]; $newOptionId = 10; @@ -196,6 +199,9 @@ public function testAddWithCannotSaveException() ], 'order' => [ 'id_new_option' => $sortOder, + ], + 'is_default' => [ + 'id_new_option' => true, ] ]; @@ -253,6 +259,9 @@ public function testUpdate(string $label): void ], 'order' => [ $optionId => $sortOder, + ], + 'is_default' => [ + $optionId => true, ] ]; 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 88daf1a8a6f52..e1aab7f44b48a 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 @@ -13,6 +13,7 @@ use Magento\Eav\Model\AttributeDataFactory; use Magento\Eav\Model\Entity\AbstractEntity; use Magento\Eav\Model\Validator\Attribute\Data; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Model\AbstractModel; use Magento\Framework\ObjectManagerInterface; @@ -35,6 +36,11 @@ class DataTest extends TestCase */ private $model; + /** + * @var \Magento\Eav\Model\Config|MockObject + */ + private $eavConfigMock; + /** * @inheritdoc */ @@ -49,7 +55,12 @@ protected function setUp(): void ] ) ->getMock(); - + $this->createMock(ObjectManagerInterface::class); + ObjectManager::setInstance($this->createMock(ObjectManagerInterface::class)); + $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->onlyMethods(['getEntityType']) + ->disableOriginalConstructor() + ->getMock(); $this->model = new Data($this->attrDataFactory); } @@ -205,13 +216,17 @@ public function testIsValidAttributesFromCollection(): void 'is_visible' => true, ] ); + $entityTypeCode = 'entity_type_code'; $collection = $this->getMockBuilder(DataObject::class) ->addMethods(['getItems'])->getMock(); $collection->expects($this->once())->method('getItems')->willReturn([$attribute]); $entityType = $this->getMockBuilder(DataObject::class) - ->addMethods(['getAttributeCollection']) + ->addMethods(['getAttributeCollection','getEntityTypeCode']) ->getMock(); + $entityType->expects($this->atMost(2))->method('getEntityTypeCode')->willReturn($entityTypeCode); $entityType->expects($this->once())->method('getAttributeCollection')->willReturn($collection); + $this->eavConfigMock->expects($this->once())->method('getEntityType') + ->with($entityTypeCode)->willReturn($entityType); $entity = $this->_getEntityMock(); $entity->expects($this->once())->method('getResource')->willReturn($resource); $entity->expects($this->once())->method('getEntityType')->willReturn($entityType); @@ -235,7 +250,7 @@ public function testIsValidAttributesFromCollection(): void )->willReturn( $dataModel ); - $validator = new Data($attrDataFactory); + $validator = new Data($attrDataFactory, $this->eavConfigMock); $validator->setData(['attribute' => 'new_test_data']); $this->assertTrue($validator->isValid($entity)); diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php new file mode 100644 index 0000000000000..5c918a2354800 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format selected options values provider for GraphQL output + */ +class GetAttributeSelectedOptionComposite implements GetAttributeSelectedOptionInterface +{ + /** + * @var GetAttributeSelectedOptionInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributeSelectedOptionInterface to use for attribute with $attributeCode + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws RuntimeException + */ + public function execute(string $entityType, array $customAttribute): ?array + { + if (!isset($this->providers[$entityType])) { + throw new RuntimeException( + __(sprintf('"%s" entity type not set in providers', $entityType)) + ); + } + if (!$this->providers[$entityType] instanceof GetAttributeSelectedOptionInterface) { + throw new RuntimeException( + __('Configured attribute selected option data providers should implement + GetAttributeSelectedOptionInterface') + ); + } + + return $this->providers[$entityType]->execute($entityType, $customAttribute); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php new file mode 100644 index 0000000000000..96d5bd6b547b1 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting custom attributes selected options. + */ +interface GetAttributeSelectedOptionInterface +{ + /** + * Retrieve all selected options of an attribute filtered by attribute code + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php new file mode 100644 index 0000000000000..288e0a5cce8eb --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attribute values provider for GraphQL output + */ +class GetAttributeValueComposite implements GetAttributeValueInterface +{ + /** + * @var GetAttributeValueInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributeValueInterface to use for attribute with $attributeCode + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws RuntimeException|LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array + { + if (!isset($this->providers[$entityType])) { + throw new RuntimeException( + __(sprintf('"%s" entity type not set in providers', $entityType)) + ); + } + if (!$this->providers[$entityType] instanceof GetAttributeValueInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeValueInterface') + ); + } + + return $this->providers[$entityType]->execute($entityType, $customAttribute); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php new file mode 100644 index 0000000000000..4f9b6f63f3153 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting custom attributes. + */ +interface GetAttributeValueInterface +{ + /** + * Retrieve all attributes filtered by attribute code + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php new file mode 100644 index 0000000000000..a19ff9c9b8bdc --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes form provider for GraphQL output + */ +class GetAttributesFormComposite implements GetAttributesFormInterface +{ + /** + * @var GetAttributesFormInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributesFormInterface to use for form with $formCode + * + * @param string $formCode + * @return array + * @throws RuntimeException + */ + public function execute(string $formCode): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributesFormInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributesFormInterface') + ); + } + + try { + return $provider->execute($formCode); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php new file mode 100644 index 0000000000000..c85054386d153 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting form attributes metadata. + */ +interface GetAttributesFormInterface +{ + /** + * Retrieve all attributes filtered by form code + * + * @param string $formCode + * @throws LocalizedException + */ + public function execute(string $formCode): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php new file mode 100644 index 0000000000000..67b2692e55794 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Retrieve EAV attributes details + */ +class GetAttributesMetadata +{ + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @var SearchCriteriaBuilderFactory + */ + private SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory; + + /** + * @var GetAttributeDataInterface + */ + private GetAttributeDataInterface $getAttributeData; + + /** + * @param AttributeRepositoryInterface $attributeRepository + * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory + * @param GetAttributeDataInterface $getAttributeData + */ + public function __construct( + AttributeRepositoryInterface $attributeRepository, + SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, + GetAttributeDataInterface $getAttributeData + ) { + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->getAttributeData = $getAttributeData; + } + + /** + * Get attribute metadata details + * + * @param array $attributesInputs + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute(array $attributesInputs, int $storeId): array + { + if (empty($attributesInputs)) { + return []; + } + + $codes = []; + $errors = []; + + foreach ($attributesInputs as $attributeInput) { + $codes[$attributeInput['entity_type']][] = $attributeInput['attribute_code']; + } + + $items = []; + + foreach ($codes as $entityType => $attributeCodes) { + $builder = $this->searchCriteriaBuilderFactory->create(); + $builder + ->addFilter('attribute_code', $attributeCodes, 'in'); + try { + $attributes = $this->attributeRepository->getList($entityType, $builder->create())->getItems(); + } catch (LocalizedException $exception) { + $errors[] = [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => (string) __('Entity "%entity" could not be found.', ['entity' => $entityType]) + ]; + continue; + } + + $notFoundCodes = array_diff($attributeCodes, $this->getCodes($attributes)); + foreach ($notFoundCodes as $notFoundCode) { + $errors[] = [ + 'type' => 'ATTRIBUTE_NOT_FOUND', + 'message' => (string) __('Attribute code "%code" could not be found.', ['code' => $notFoundCode]) + ]; + } + foreach ($attributes as $attribute) { + if (method_exists($attribute, 'getIsVisible') && !$attribute->getIsVisible()) { + continue; + } + $items[] = $this->getAttributeData->execute($attribute, $entityType, $storeId); + } + } + + return [ + 'items' => $items, + 'errors' => $errors + ]; + } + + /** + * Retrieve an array of codes from the array of attributes + * + * @param AttributeInterface[] $attributes + * @return AttributeInterface[] + */ + private function getCodes(array $attributes): array + { + return array_map( + function (AttributeInterface $attribute) { + return $attribute->getAttributeCode(); + }, + $attributes + ); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php new file mode 100644 index 0000000000000..407c77573d6c0 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php @@ -0,0 +1,119 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\GraphQl\Query\EnumLookup; + +/** + * Format attributes for GraphQL output + */ +class GetAttributeData implements GetAttributeDataInterface +{ + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @param EnumLookup $enumLookup + */ + public function __construct(EnumLookup $enumLookup) + { + $this->enumLookup = $enumLookup; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + return [ + 'id' => $attribute->getAttributeId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel($storeId), + 'sort_order' => $attribute->getPosition(), + 'entity_type' => $this->enumLookup->getEnumValueFromField( + 'AttributeEntityTypeEnum', + $entityType + ), + 'frontend_input' => $this->getFrontendInput($attribute), + 'frontend_class' => $attribute->getFrontendClass(), + 'is_required' => $attribute->getIsRequired(), + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => $attribute->getIsUnique(), + 'options' => $this->getOptions($attribute), + 'attribute' => $attribute + ]; + } + + /** + * Returns default frontend input for attribute if not set + * + * @param AttributeInterface $attribute + * @return string + */ + private function getFrontendInput(AttributeInterface $attribute): string + { + if ($attribute->getFrontendInput() === null) { + return "UNDEFINED"; + } + return $this->enumLookup->getEnumValueFromField( + 'AttributeFrontendInputEnum', + $attribute->getFrontendInput() + ); + } + + /** + * Retrieve formatted attribute options + * + * @param AttributeInterface $attribute + * @return array + */ + private function getOptions(AttributeInterface $attribute): array + { + if (!$attribute->getOptions()) { + return []; + } + return array_filter( + array_map( + function (AttributeOptionInterface $option) use ($attribute) { + if (is_array($option->getValue())) { + $value = (empty($option->getValue()) ? '' : (string)$option->getValue()[0]['value']); + } else { + $value = (string)$option->getValue(); + } + $label = (string)$option->getLabel(); + if (empty(trim($value)) && empty(trim($label))) { + return null; + } + return [ + 'label' => $label, + 'value' => $value, + 'is_default' => $attribute->getDefaultValue() ? + in_array($value, explode(',', $attribute->getDefaultValue())) : null + ]; + }, + $attribute->getOptions() + ) + ); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php new file mode 100644 index 0000000000000..30fdc0dba5ab3 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes for GraphQL output + */ +class GetAttributeDataComposite implements GetAttributeDataInterface +{ + /** + * @var GetAttributeDataInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + $data = []; + + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeDataInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeDataInterface') + ); + } + $data[] = $provider->execute($attribute, $entityType, $storeId); + } + + return array_merge([], ...$data); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php new file mode 100644 index 0000000000000..2a586c824efae --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes for GraphQL output + */ +interface GetAttributeDataInterface +{ + /** + * Retrieve formatted attribute metadata + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute(AttributeInterface $attribute, string $entityType, int $storeId): array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php new file mode 100644 index 0000000000000..56903cda7a147 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attribute values provider for GraphQL output + */ +class GetAttributeValueComposite implements GetAttributeValueInterface +{ + /** + * @var GetAttributeValueInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * @inheritdoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeValueInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeValueInterface') + ); + } + + try { + return $provider->execute($entity, $code, $value); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php new file mode 100644 index 0000000000000..65a4124fb6ef0 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +/** + * Interface for getting custom attributes. + */ +interface GetAttributeValueInterface +{ + /** + * Retrieve all attributes filtered by attribute code + * + * @param string $entity + * @param string $code + * @param string $value + * @return array|null + */ + public function execute(string $entity, string $code, string $value): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php new file mode 100644 index 0000000000000..91eb721b4d822 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionInterface; + +/** + * Custom attribute value provider for customer + */ +class GetCustomAttributes implements GetAttributeValueInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var GetAttributeSelectedOptionInterface + */ + private GetAttributeSelectedOptionInterface $getAttributeSelectedOption; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param GetAttributeSelectedOptionInterface $getAttributeSelectedOption + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + GetAttributeSelectedOptionInterface $getAttributeSelectedOption, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + $this->getAttributeSelectedOption = $getAttributeSelectedOption; + } + + /** + * @inheritDoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + $attr = $this->attributeRepository->get($entity, $code); + + $result = [ + 'entity_type' => $entity, + 'code' => $code, + 'sort_order' => $attr->getSortOrder() ?? '' + ]; + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + $result['selected_options'] = $this->getAttributeSelectedOption->execute($entity, $code, $value); + } else { + $result['value'] = $value; + } + return $result; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php new file mode 100644 index 0000000000000..fb7cc4134aa88 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format selected options values provider for GraphQL output + */ +class GetAttributeSelectedOptionComposite implements GetAttributeSelectedOptionInterface +{ + /** + * @var GetAttributeSelectedOptionInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * @inheritdoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeSelectedOptionInterface) { + throw new RuntimeException( + __('Configured attribute selected option data providers should implement + GetAttributeSelectedOptionInterface') + ); + } + + try { + return $provider->execute($entity, $code, $value); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php new file mode 100644 index 0000000000000..b4230ea60c958 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +/** + * Interface for getting custom attributes seelcted options. + */ +interface GetAttributeSelectedOptionInterface +{ + /** + * Retrieve all selected options of an attribute filtered by attribute code + * + * @param string $entity + * @param string $code + * @param string $value + * @return array|null + */ + public function execute(string $entity, string $code, string $value): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php new file mode 100644 index 0000000000000..c84a7bdd5134d --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +use Magento\Eav\Model\AttributeRepository; + +/** + * Custom attribute value provider for customer + */ +class GetCustomSelectedOptionAttributes implements GetAttributeSelectedOptionInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @param AttributeRepository $attributeRepository + */ + public function __construct(AttributeRepository $attributeRepository) + { + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + $attribute = $this->attributeRepository->get($entity, $code); + + $result = []; + $selectedValues = explode(',', $value); + foreach ($attribute->getOptions() as $option) { + if (!in_array($option->getValue(), $selectedValues)) { + continue; + } + $result[] = [ + 'value' => $option->getValue(), + 'label' => $option->getLabel() + ]; + } + return $result; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php new file mode 100644 index 0000000000000..257c7c00da588 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\EavGraphQl\Model\GetAttributesFormComposite; +use Magento\EavGraphQl\Model\GetAttributesMetadata; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Load EAV attributes associated to a form + */ +class AttributesForm implements ResolverInterface +{ + /** + * @var GetAttributesFormComposite $getAttributesFormComposite + */ + private GetAttributesFormComposite $getAttributesFormComposite; + + /** + * @var GetAttributesMetadata + */ + private GetAttributesMetadata $getAttributesMetadata; + + /** + * @param GetAttributesFormComposite $providerFormComposite + * @param GetAttributesMetadata $getAttributesMetadata + */ + public function __construct( + GetAttributesFormComposite $providerFormComposite, + GetAttributesMetadata $getAttributesMetadata + ) { + $this->getAttributesFormComposite = $providerFormComposite; + $this->getAttributesMetadata = $getAttributesMetadata; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + + if (empty($args['formCode'])) { + throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'formCode')); + } + + $formCode = $args['formCode']; + + $attributes = $this->getAttributesFormComposite->execute($formCode); + if ($this->isAnAdminForm($formCode) || $attributes === null) { + return [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => (string) __('Form "%form" could not be found.', ['form' => $formCode]) + ] + ] + ]; + } + + return array_merge( + [ + 'formCode' => $formCode + ], + $this->getAttributesMetadata->execute( + $attributes, + (int)$context->getExtensionAttributes()->getStore()->getId() + ) + ); + } + + /** + * Check if passed form formCode is an admin form. + * + * @param string $formCode + * @return bool + */ + private function isAnAdminForm(string $formCode): bool + { + return str_starts_with($formCode, 'adminhtml_'); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php new file mode 100644 index 0000000000000..91c00a7ad69cc --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Returns a list of attributes metadata for a given entity type. + */ +class AttributesList implements ResolverInterface +{ + /** + * @var GetAttributeDataInterface + */ + private GetAttributeDataInterface $getAttributeData; + + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @var GetFilteredAttributes + */ + private GetFilteredAttributes $getFilteredAttributes; + + /** + * @param EnumLookup $enumLookup + * @param GetAttributeDataInterface $getAttributeData + * @param GetFilteredAttributes $getFilteredAttributes + */ + public function __construct( + EnumLookup $enumLookup, + GetAttributeDataInterface $getAttributeData, + GetFilteredAttributes $getFilteredAttributes, + ) { + $this->enumLookup = $enumLookup; + $this->getAttributeData = $getAttributeData; + $this->getFilteredAttributes = $getFilteredAttributes; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (!$args['entityType']) { + throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'entityType')); + } + + $storeId = (int) $context->getExtensionAttributes()->getStore()->getId(); + $entityType = $this->enumLookup->getEnumValueFromField( + 'AttributeEntityTypeEnum', + strtolower($args['entityType']) + ); + + $filterArgs = $args['filters'] ?? []; + + $attributesList = $this->getFilteredAttributes->execute($filterArgs, strtolower($entityType)); + + return [ + 'items' => $this->getAttributesMetadata($attributesList['items'], $entityType, $storeId), + 'entity_type' => $entityType, + 'errors' => $attributesList['errors'] + ]; + } + + /** + * Returns formatted list of attributes + * + * @param AttributeInterface[] $attributesList + * @param string $entityType + * @param int $storeId + * + * @return array[] + * @throws RuntimeException + */ + private function getAttributesMetadata(array $attributesList, string $entityType, int $storeId): array + { + return array_map(function (AttributeInterface $attribute) use ($entityType, $storeId): array { + return $this->getAttributeData->execute($attribute, strtolower($entityType), $storeId); + }, $attributesList); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php new file mode 100644 index 0000000000000..f4f9853c1b6ab --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\EavGraphQl\Model\GetAttributesMetadata; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Load EAV attributes by attribute_code and entity_type + */ +class AttributesMetadata implements ResolverInterface +{ + /** + * @var GetAttributesMetadata + */ + private GetAttributesMetadata $getAttributesMetadata; + + /** + * @param GetAttributesMetadata $getAttributesMetadata + */ + public function __construct( + GetAttributesMetadata $getAttributesMetadata + ) { + $this->getAttributesMetadata = $getAttributesMetadata; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $attributeInputs = $args['attributes']; + + if (empty($attributeInputs)) { + throw new GraphQlInputException( + __( + 'Required parameters "attribute_code" and "entity_type" of type String.' + ) + ); + } + + foreach ($attributeInputs as $attributeInput) { + if (!isset($attributeInput['attribute_code'])) { + throw new GraphQlInputException(__('The attribute_code is required to retrieve the metadata')); + } + if (!isset($attributeInput['entity_type'])) { + throw new GraphQlInputException(__('The entity_type is required to retrieve the metadata')); + } + } + + return $this->getAttributesMetadata->execute( + $attributeInputs, + (int) $context->getExtensionAttributes()->getStore()->getId() + ); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php new file mode 100644 index 0000000000000..d02542e54d336 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute; + +/** + * Cache identity provider for attributes list query results. + */ +class AttributesListIdentity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData['entity_type']) || $resolvedData['entity_type'] === "") { + return []; + } + + $identities = [ + Config::ENTITIES_CACHE_ID . "_" . $resolvedData['entity_type'] . "_ENTITY" + ]; + + if (empty($resolvedData['items']) || !is_array($resolvedData['items'][0])) { + return $identities; + } + + foreach ($resolvedData['items'] as $item) { + if ($item['attribute'] instanceof AttributeInterface) { + $identities[] = sprintf( + "%s_%s", + Attribute::CACHE_TAG, + $item['attribute']->getAttributeId() + ); + } + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php new file mode 100644 index 0000000000000..25df607cfbde2 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Eav\Model\Entity\Attribute as EavAttribute; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Cache identity provider for custom attribute metadata query results. + */ +class CustomAttributeMetadataIdentity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $identities = []; + if (isset($resolvedData['items']) && !empty($resolvedData['items'])) { + foreach ($resolvedData['items'] as $item) { + if (is_array($item)) { + $identities[] = sprintf( + "%s_%s_%s", + EavAttribute::CACHE_TAG, + $item['entity_type'], + $item['attribute_code'] + ); + } + } + } else { + return []; + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php new file mode 100644 index 0000000000000..2b67b43d1e9ac --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Eav\Model\Entity\Attribute as EavAttribute; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Cache identity provider for custom attribute metadata query results. + */ +class CustomAttributeMetadataV2Identity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $identities = []; + if (isset($resolvedData['items']) && !empty($resolvedData['items'])) { + foreach ($resolvedData['items'] as $item) { + if (is_array($item)) { + $identities[] = sprintf( + "%s_%s", + EavAttribute::CACHE_TAG, + $item['id'] + ); + } + } + } else { + return []; + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 0ebeca2929757..df4761c22101e 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -17,6 +17,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\NotFoundException; /** * Resolve data for custom attribute metadata requests @@ -52,7 +53,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) { + ): array { $attributes['items'] = null; $attributeInputs = $args['attributes']; foreach ($attributeInputs as $attributeInput) { @@ -123,7 +124,8 @@ private function getStorefrontProperties(AttributeInterface $attribute) * * @return string[] */ - private function getLayeredNavigationPropertiesEnum() { + private function getLayeredNavigationPropertiesEnum() + { return [ 0 => 'NO', 1 => 'FILTERABLE_WITH_RESULTS', diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php new file mode 100644 index 0000000000000..0d852b401eac2 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\ResourceConnection; + +/** + * + * Check if the fields belongs to an entity + */ +class EntityFieldChecker +{ + /*** + * @var ResourceConnection + */ + private ResourceConnection $resource; + + /** + * @var Type + */ + private Type $eavEntityType; + + /** + * @param ResourceConnection $resource + * @param Type $eavEntityType + */ + public function __construct(ResourceConnection $resource, Type $eavEntityType) + { + $this->resource = $resource; + $this->eavEntityType = $eavEntityType; + } + + /** + * Check if the field exists on the entity + * + * @param string $entityTypeCode + * @param string $field + * @return bool + */ + public function fieldBelongToEntity(string $entityTypeCode, string $field): bool + { + $connection = $this->resource->getConnection(); + $columns = $connection->describeTable( + $this->eavEntityType->loadByCode($entityTypeCode)->getAdditionalAttributeTable() + ); + + return array_key_exists($field, $columns); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php new file mode 100644 index 0000000000000..3b402a6e0dfb5 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; + +/** + * Return attributes filtered and errors if there is some filter that cannot be applied + */ +class GetFilteredAttributes +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private SearchCriteriaBuilder $searchCriteriaBuilder; + + /** + * @var EntityFieldChecker + */ + private EntityFieldChecker $entityFieldChecker; + + /** + * @param AttributeRepository $attributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param EntityFieldChecker $entityFieldChecker + */ + public function __construct( + AttributeRepository $attributeRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + EntityFieldChecker $entityFieldChecker + ) { + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->entityFieldChecker = $entityFieldChecker; + } + + /** + * Return the attributes filtered and errors if the filter could not be applied + * + * @param array $filterArgs + * @param string $entityType + * @return array + * @throws InputException + */ + public function execute(array $filterArgs, string $entityType): array + { + $errors = []; + foreach ($filterArgs as $field => $value) { + if ($this->entityFieldChecker->fieldBelongToEntity(strtolower($entityType), $field)) { + $this->searchCriteriaBuilder->addFilter($field, $value); + } else { + $errors[] = [ + 'type' => 'FILTER_NOT_FOUND', + 'message' => + (string)__( + 'Cannot filter by "%filter" as that field does not belong to "%entity".', + ['filter' => $field, 'entity' => $entityType] + ) + ]; + } + } + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('is_visible', true) + ->addFilter('backend_type', 'static', 'neq') + ->create(); + + $attributesList = $this->attributeRepository->getList(strtolower($entityType), $searchCriteria)->getItems(); + + return [ + 'items' => $attributesList, + 'errors' => $errors + ]; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php new file mode 100644 index 0000000000000..c4fd8403bd670 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeMetadata implements TypeResolverInterface +{ + private const TYPE = 'AttributeMetadata'; + + /** + * @var string[] + */ + private array $entityTypes; + + /** + * @param array $entityTypes + */ + public function __construct(array $entityTypes = []) + { + $this->entityTypes = $entityTypes; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + if (!isset($data['entity_type'])) { + return self::TYPE; + } + return $this->entityTypes[$data['entity_type']] ?? self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php new file mode 100644 index 0000000000000..0390f3aaf99ab --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeOption implements TypeResolverInterface +{ + private const TYPE = 'AttributeOptionMetadata'; + + /** + * @var TypeResolverInterface[] + */ + private array $typeResolvers; + + /** + * @param array $typeResolvers + */ + public function __construct(array $typeResolvers = []) + { + $this->typeResolvers = $typeResolvers; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php new file mode 100644 index 0000000000000..5a436b911ed09 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Eav\Model\Attribute; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeSelectedOption implements TypeResolverInterface +{ + private const TYPE = 'AttributeSelectedOption'; + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php new file mode 100644 index 0000000000000..bafcd4a11f490 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Eav\Model\Attribute; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeValue implements TypeResolverInterface +{ + private const TYPE = 'AttributeValue'; + + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + /** @var Attribute $attr */ + $attr = $this->attributeRepository->get( + $data['entity_type'], + $data['code'], + ); + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + return 'AttributeSelectedOptions'; + } + + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php new file mode 100644 index 0000000000000..dd713f4f69acd --- /dev/null +++ b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Plugin\Eav; + +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Api\AttributeInterface; + +/** + * EAV plugin runs page cache clean and provides proper EAV identities. + */ +class AttributePlugin +{ + /** + * Clean cache by relevant tags after entity save. + * + * @param Attribute $subject + * @param array $result + * + * @return string[] + */ + public function afterGetIdentities(Attribute $subject, array $result): array + { + return array_merge( + $result, + [ + sprintf( + "%s_%s_%s", + Attribute::CACHE_TAG, + $subject->getEntityType()->getEntityTypeCode(), + $subject->getOrigData(AttributeInterface::ATTRIBUTE_CODE) + ?? $subject->getData(AttributeInterface::ATTRIBUTE_CODE) + ) + ] + ); + } +} diff --git a/app/code/Magento/EavGraphQl/README.md b/app/code/Magento/EavGraphQl/README.md index 6bf418c798dec..be4879ac18ceb 100644 --- a/app/code/Magento/EavGraphQl/README.md +++ b/app/code/Magento/EavGraphQl/README.md @@ -10,6 +10,6 @@ For information about enabling or disabling a module in Magento 2, see [Enable o You can get more information at articles: -- [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +- [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). - [customAttributeMetadata query](https://developer.adobe.com/commerce/webapi/graphql/schema/store/queries/custom-attribute-metadata/). - [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html) diff --git a/app/code/Magento/EavGraphQl/etc/di.xml b/app/code/Magento/EavGraphQl/etc/di.xml new file mode 100644 index 0000000000000..30c4e72258512 --- /dev/null +++ b/app/code/Magento/EavGraphQl/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Eav\Model\Entity\Attribute"> + <plugin name="entityAttributeChangePlugin" type="Magento\EavGraphQl\Plugin\Eav\AttributePlugin" /> + </type> +</config> diff --git a/app/code/Magento/EavGraphQl/etc/graphql/di.xml b/app/code/Magento/EavGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..93726a50a9c0b --- /dev/null +++ b/app/code/Magento/EavGraphQl/etc/graphql/di.xml @@ -0,0 +1,72 @@ +<?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\EavGraphQl\Model\Output\GetAttributeDataInterface" type="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"/> + <preference for="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface" type="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueComposite"/> + <preference for="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionInterface" type="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionComposite"/> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="eav_attribute_data" xsi:type="object">Magento\EavGraphQl\Model\Output\GetAttributeData</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeFrontendInputEnum" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> + <item name="date" xsi:type="string">date</item> + <item name="datetime" xsi:type="string">datetime</item> + <item name="file" xsi:type="string">file</item> + <item name="gallery" xsi:type="string">gallery</item> + <item name="hidden" xsi:type="string">hidden</item> + <item name="image" xsi:type="string">image</item> + <item name="media_image" xsi:type="string">media_image</item> + <item name="multiline" xsi:type="string">multiline</item> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="price" xsi:type="string">price</item> + <item name="select" xsi:type="string">select</item> + <item name="text" xsi:type="string">text</item> + <item name="textarea" xsi:type="string">textarea</item> + <item name="weight" xsi:type="string">weight</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\EavGraphQl\Model\Output\Value\GetCustomAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\GetCustomAttributes"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\EavGraphQl\Model\Output\Value\Options\GetCustomSelectedOptionAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeValue"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 25f53c4ad7ea8..3323a63622f44 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -2,7 +2,24 @@ # See COPYING.txt for license details. type Query { - customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "Return the attribute type, given an attribute code and entity type.") @cache(cacheable: false) + customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")): + CustomAttributeMetadata + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") + @doc(description: "Return the attribute type, given an attribute code and entity type.") + @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataIdentity") + @deprecated(reason: "Use `customAttributeMetadataV2` query instead.") + customAttributeMetadataV2(attributes: [AttributeInput!]): AttributesMetadataOutput! @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesMetadata") @doc(description: "Retrieve EAV attributes metadata.") @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataV2Identity") + attributesForm(formCode: String! @doc(description: "Form code.")): AttributesFormOutput! + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesForm") + @doc(description: "Retrieve EAV attributes associated to a frontend form.") + @cache(cacheIdentity: "Magento\\Eav\\Model\\Cache\\AttributesFormIdentity") + attributesList( + entityType: AttributeEntityTypeEnum! @doc(description: "Entity type.") + filters: AttributeFilterInput @doc(description: "Identifies which filter inputs to search for and return.") + ): AttributesMetadataOutput + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesList") + @doc(description: "Returns a list of attributes metadata for a given entity type.") + @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\AttributesListIdentity") } type CustomAttributeMetadata @doc(description: "Defines an array of custom attributes.") { @@ -41,3 +58,115 @@ input AttributeInput @doc(description: "Defines the attribute characteristics to attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") entity_type: String @doc(description: "The type of entity that defines the attribute.") } + +type AttributesMetadataOutput @doc(description: "Metadata of EAV attributes.") { + items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.") + errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.") +} + +type AttributeMetadataError @doc(description: "Attribute metadata retrieval error.") { + type: AttributeMetadataErrorType! @doc(description: "Attribute metadata retrieval error type.") + message: String! @doc(description: "Attribute metadata retrieval error message.") +} + +enum AttributeMetadataErrorType @doc(description: "Attribute metadata retrieval error types.") { + ENTITY_NOT_FOUND @doc(description: "The requested entity was not found.") + ATTRIBUTE_NOT_FOUND @doc(description: "The requested attribute was not found.") + FILTER_NOT_FOUND @doc(description: "The filter cannot be applied as it does not belong to the entity") + UNDEFINED @doc(description: "Not categorized error, see the error message.") +} + +interface CustomAttributeMetadataInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeMetadata") @doc(description: "An interface containing fields that define the EAV attribute."){ + code: ID! @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") + label: String @doc(description: "The label assigned to the attribute.") + entity_type: AttributeEntityTypeEnum! @doc(description: "The type of entity that defines the attribute.") + frontend_input: AttributeFrontendInputEnum @doc(description: "The frontend input type of the attribute.") + frontend_class: String @doc(description: "The frontend class of the attribute.") + is_required: Boolean! @doc(description: "Whether the attribute value is required.") + default_value: String @doc(description: "Default attribute value.") + is_unique: Boolean! @doc(description: "Whether the attribute value must be unique.") + options: [CustomAttributeOptionInterface!]! @doc(description: "Attribute options.") +} + +interface CustomAttributeOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeOption") { + label: String! @doc(description: "The label assigned to the attribute option.") + value: String! @doc(description: "The attribute option value.") + is_default: Boolean @doc(description: "Is the option value default.") +} + +type AttributeOptionMetadata implements CustomAttributeOptionInterface @doc(description: "Base EAV implementation of CustomAttributeOptionInterface.") { +} + +type AttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Base EAV implementation of CustomAttributeMetadataInterface.") { +} + +enum AttributeEntityTypeEnum @doc(description: "List of all entity types. Populated by the modules introducing EAV entities.") { +} + +enum AttributeFrontendInputEnum @doc(description: "EAV attribute frontend input types.") { + BOOLEAN + DATE + DATETIME + FILE + GALLERY + HIDDEN + IMAGE + MEDIA_IMAGE + MULTILINE + MULTISELECT + PRICE + SELECT + TEXT + TEXTAREA + WEIGHT + UNDEFINED +} + +type AttributesFormOutput @doc(description: "Metadata of EAV attributes associated to form") { + items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.") + errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.") +} + +interface AttributeValueInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeValue") { + code: ID! @doc(description: "The attribute code.") +} + +type AttributeValue implements AttributeValueInterface { + value: String! @doc(description: "The attribute value.") +} + +type AttributeSelectedOptions implements AttributeValueInterface { + selected_options: [AttributeSelectedOptionInterface!]! +} + +interface AttributeSelectedOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeSelectedOption") { + label: String! @doc(description: "The attribute selected option label.") + value: String! @doc(description: "The attribute selected option value.") +} + +type AttributeSelectedOption implements AttributeSelectedOptionInterface { +} + +input AttributeValueInput @doc(description: "Specifies the value for attribute.") { + attribute_code: String! @doc(description: "The code of the attribute.") + value: String @doc(description: "The value assigned to the attribute.") + selected_options: [AttributeInputSelectedOption!] @doc(description: "An array containing selected options for a select or multiselect attribute.") +} + +input AttributeInputSelectedOption @doc(description: "Specifies selected option for a select or multiselect attribute value.") { + value: String! @doc(description: "The attribute option value.") +} + +input AttributeFilterInput @doc(description: "An input object that specifies the filters used for attributes.") { + is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.") + is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.") + is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.") + is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.") + is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.") + is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.") + is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.") + is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.") + is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.") + is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.") + used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.") +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index a9fb67f209aa7..c1772086d7ba3 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -250,6 +250,7 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, * - "Visible in Advanced Search" (is_visible_in_advanced_search) * - "Use in Layered Navigation" (is_filterable) * - "Use in Search Results Layered Navigation" (is_filterable_in_search) + * - "Use in Sorting in Product Listing" (used_for_sort_by) * * @param Attribute $attribute * @return bool @@ -261,6 +262,7 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool || $attribute->getIsVisibleInAdvancedSearch() || $attribute->getIsFilterable() || $attribute->getIsFilterableInSearch() + || $attribute->getUsedForSortBy() ); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php index 75636991e7ee6..e97a46eed7d39 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -10,42 +10,43 @@ use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; /** * Provide attribute adapter. */ -class AttributeProvider +class AttributeProvider implements ResetAfterRequestInterface { /** * Object Manager instance * * @var ObjectManagerInterface */ - private $objectManager; + private ObjectManagerInterface $objectManager; /** * Instance name to create * * @var string */ - private $instanceName; + private string $instanceName; /** * @var Config */ - private $eavConfig; + private Config $eavConfig; /** * @var array */ - private $cachedPool = []; + private array $cachedPool = []; /** * @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; /** * Factory constructor @@ -101,4 +102,12 @@ public function removeAttributeCacheByCode(string $attributeCode): void unset($this->cachedPool[$attributeCode]); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cachedPool = []; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index 1db0bad36e243..87712641c05d4 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -18,6 +18,7 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; @@ -29,9 +30,9 @@ class DynamicField implements FieldProviderInterface /** * Category collection. * - * @var Collection + * @var CollectionFactory */ - private $categoryCollection; + private $categoryCollectionFactory; /** * Customer group repository. @@ -41,8 +42,6 @@ class DynamicField implements FieldProviderInterface private $groupRepository; /** - * Search criteria builder. - * * @var SearchCriteriaBuilder */ private $searchCriteriaBuilder; @@ -79,8 +78,10 @@ class DynamicField implements FieldProviderInterface * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param FieldNameResolver $fieldNameResolver * @param AttributeProvider $attributeAdapterProvider - * @param Collection $categoryCollection + * @param Collection $categoryCollection @deprecated @see $categoryCollectionFactory * @param StoreManagerInterface|null $storeManager + * @param CollectionFactory|null $categoryCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( FieldTypeConverterInterface $fieldTypeConverter, @@ -90,7 +91,8 @@ public function __construct( FieldNameResolver $fieldNameResolver, AttributeProvider $attributeAdapterProvider, Collection $categoryCollection, - ?StoreManagerInterface $storeManager = null + ?StoreManagerInterface $storeManager = null, + ?CollectionFactory $categoryCollectionFactory = null ) { $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; @@ -98,7 +100,8 @@ public function __construct( $this->indexTypeConverter = $indexTypeConverter; $this->fieldNameResolver = $fieldNameResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; - $this->categoryCollection = $categoryCollection; + $this->categoryCollectionFactory = $categoryCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } @@ -108,7 +111,7 @@ public function __construct( public function getFields(array $context = []): array { $allAttributes = []; - $categoryIds = $this->categoryCollection->getAllIds(); + $categoryIds = $this->categoryCollectionFactory->create()->getAllIds(); $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); foreach ($categoryIds as $categoryId) { diff --git a/app/code/Magento/Elasticsearch/README.md b/app/code/Magento/Elasticsearch/README.md index 835cd4ab37f19..8a58ddfde39a7 100644 --- a/app/code/Magento/Elasticsearch/README.md +++ b/app/code/Magento/Elasticsearch/README.md @@ -1,7 +1,7 @@ -#Magento_Elasticsearch module +# Magento_Elasticsearch module -Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module -provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds +Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module +provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds support for Elasticsearch v5. The module implements Magento_Search library interfaces. diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml index 3c3bac70f4dc2..8451ff03a1344 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-6395"/> <group value="catalog_search"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="setOutOfStockToYes"/> diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index 9f1b59b1bfc81..354ad01f14a64 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -373,6 +373,57 @@ public static function mapProvider(): array [10 => '44', 11 => '45'], ['color' => [44, 45], 'color_value' => ['red', 'black']], ], + 'select with options with sort by and filterable' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => true, + 'used_for_sort_by' => true, + 'is_filterable_in_grid' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + [10 => '44', 11 => '45'], + ['color' => [44, 45], 'color_value' => ['red', 'black']], + ], + 'unsearchable select with options with sort by and filterable' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => false, + 'used_for_sort_by' => false, + 'is_filterable_in_grid' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + '44', + ['color' => 44], + ], + 'select with options with sort by only' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => false, + 'used_for_sort_by' => true, + 'is_filterable_in_grid' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + [10 => '44', 11 => '45'], + ['color' => [44, 45], 'color_value' => ['red', 'black']], + ], 'multiselect without options' => [ 10, [ 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 a48f65e1b6d75..280284b7a7cb5 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -111,7 +111,7 @@ class ElasticsearchTest extends TestCase */ protected function setUp(): void { - if (!class_exists(\Elasticsearch\Client::class)) { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 2cf8c9f6a3fa9..6b0e3961a8fc3 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -8,6 +8,7 @@ namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\Data\GroupSearchResultsInterface; use Magento\Customer\Api\GroupRepositoryInterface; @@ -111,8 +112,13 @@ protected function setUp(): void ->disableOriginalConstructor() ->onlyMethods(['getAllIds']) ->getMock(); + $categoryCollection = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + $categoryCollection->method('create') + ->willReturn($this->categoryCollection); $this->storeManager = $this->createMock(StoreManagerInterface::class); - $this->provider = new DynamicField( $this->fieldTypeConverter, $this->indexTypeConverter, @@ -121,7 +127,8 @@ protected function setUp(): void $this->fieldNameResolver, $this->attributeAdapterProvider, $this->categoryCollection, - $this->storeManager + $this->storeManager, + $categoryCollection ); } diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 9e6d4ceaf16e3..714890fd5f452 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~7.17.0" + "elasticsearch/elasticsearch": "~7.17.0 || ~8.5.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch7/README.md b/app/code/Magento/Elasticsearch7/README.md index d520f5efc3b91..a0c4063da5d3e 100644 --- a/app/code/Magento/Elasticsearch7/README.md +++ b/app/code/Magento/Elasticsearch7/README.md @@ -1,4 +1,4 @@ -#Magento_Elasticsearch7 module +# Magento_Elasticsearch7 module Magento_Elasticsearch7 module allows using ElasticSearch engine 7.x version for the product searching capabilities. diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php index 3dca34b09cb78..ab702e6441341 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php @@ -226,7 +226,7 @@ public function testGetItemsWithEnabledSearchSuggestion(): void */ public function testGetItemsException(): void { - if (!class_exists(\Elasticsearch\Client::class)) { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); } diff --git a/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php deleted file mode 100644 index 8168819d79a3b..0000000000000 --- a/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Block\Adminhtml\System\Config; - -/** - * Elasticsearch 8.x test connection block - */ -class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection -{ - /** - * @inheritdoc - */ - public function _getFieldMapping(): array - { - $fields = [ - 'engine' => 'catalog_search_engine', - 'hostname' => 'catalog_search_elasticsearch8_server_hostname', - 'port' => 'catalog_search_elasticsearch8_server_port', - 'index' => 'catalog_search_elasticsearch8_index_prefix', - 'enableAuth' => 'catalog_search_elasticsearch8_enable_auth', - 'username' => 'catalog_search_elasticsearch8_username', - 'password' => 'catalog_search_elasticsearch8_password', - 'timeout' => 'catalog_search_elasticsearch8_server_timeout', - ]; - - return array_merge(parent::_getFieldMapping(), $fields); - } -} diff --git a/app/code/Magento/Elasticsearch8/LICENSE.txt b/app/code/Magento/Elasticsearch8/LICENSE.txt deleted file mode 100644 index 49525fd99da9c..0000000000000 --- a/app/code/Magento/Elasticsearch8/LICENSE.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Open Software License ("OSL") v. 3.0 - -This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Open Software License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt b/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt deleted file mode 100644 index f39d641b18a19..0000000000000 --- a/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php deleted file mode 100644 index 96b45ca2f75e7..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\DynamicTemplates; - -/** - * @inheridoc - */ -class IntegerMapper implements MapperInterface -{ - /** - * @inheritdoc - */ - public function processTemplates(array $templates): array - { - $templates[] = [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ]; - - return $templates; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php deleted file mode 100644 index f4a40ae475b92..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\DynamicTemplates; - -/** - * Elasticsearch dynamic templates mapper. - */ -interface MapperInterface -{ - /** - * Add/remove/edit dynamic template mapping. - * - * @param array $templates - * @return array - */ - public function processTemplates(array $templates): array; -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PositionMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PositionMapper.php deleted file mode 100644 index d4ec8702ef81f..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PositionMapper.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\DynamicTemplates; - -/** - * @inheridoc - */ -class PositionMapper implements MapperInterface -{ - /** - * @inheritdoc - */ - public function processTemplates(array $templates): array - { - $templates[] = [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ]; - - return $templates; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php deleted file mode 100644 index c67c92deedffa..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\DynamicTemplates; - -/** - * @inheridoc - */ -class PriceMapper implements MapperInterface -{ - /** - * @inheritdoc - */ - public function processTemplates(array $templates): array - { - $templates[] = [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ]; - - return $templates; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php deleted file mode 100644 index 4a08d1760d66a..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\DynamicTemplates; - -/** - * @inheridoc - */ -class StringMapper implements MapperInterface -{ - /** - * @inheritdoc - */ - public function processTemplates(array $templates): array - { - $templates[] = [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => true, - 'copy_to' => '_search', - ], - ], - ]; - - return $templates; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php deleted file mode 100644 index db016e2a101f9..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter; - -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\MapperInterface; -use Magento\Framework\Exception\InvalidArgumentException; - -/** - * Elasticsearch dynamic templates provider. - */ -class DynamicTemplatesProvider -{ - /** - * @var array - */ - private array $mappers; - - /** - * @param MapperInterface[] $mappers - */ - public function __construct(array $mappers) - { - $this->mappers = $mappers; - } - - /** - * Get elasticsearch dynamic templates. - * - * @return array - * @throws InvalidArgumentException - */ - public function getTemplates(): array - { - $templates = []; - foreach ($this->mappers as $mapper) { - if (!$mapper instanceof MapperInterface) { - throw new InvalidArgumentException( - __('Mapper %1 should implement %2', get_class($mapper), MapperInterface::class) - ); - } - $templates = $mapper->processTemplates($templates); - } - - return $templates; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php deleted file mode 100644 index 6d3ca2add8678..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter; - -/** - * Elasticsearch adapter - */ -class Elasticsearch extends \Magento\Elasticsearch\Model\Adapter\Elasticsearch -{ - /** - * Reformat documents array to bulk format - * - * @param array $documents - * @param string $indexName - * @param string $action - * @return array - */ - public function getDocsArrayInBulkIndexFormat( - $documents, - $indexName, - $action = self::BULK_ACTION_INDEX - ): array { - $bulkArray = [ - 'index' => $indexName, - 'body' => [], - 'refresh' => true, - ]; - - foreach ($documents as $id => $document) { - $bulkArray['body'][] = [ - $action => [ - '_id' => $id, - '_index' => $indexName - ] - ]; - - if ($action == self::BULK_ACTION_INDEX) { - $bulkArray['body'][] = $document; - } - } - - return $bulkArray; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php deleted file mode 100644 index faa81cc2ab298..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; - -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; - -/** - * Default name resolver for Elasticsearch 8 - */ -class DefaultResolver implements ResolverInterface -{ - /** - * @var ResolverInterface - */ - private ResolverInterface $baseResolver; - - /** - * DefaultResolver constructor. - * @param ResolverInterface $baseResolver - */ - public function __construct(ResolverInterface $baseResolver) - { - $this->baseResolver = $baseResolver; - } - - /** - * Get field name. - * - * @param AttributeAdapter $attribute - * @param array $context - * @return string|null - */ - public function getFieldName(AttributeAdapter $attribute, $context = []): ?string - { - $fieldName = $this->baseResolver->getFieldName($attribute, $context); - if ($fieldName === '_all') { - $fieldName = '_search'; - } - - return $fieldName; - } -} diff --git a/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php deleted file mode 100644 index d43551d5c81f2..0000000000000 --- a/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php +++ /dev/null @@ -1,406 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Model\Client; - -use Elastic\Elasticsearch\Client; -use Elastic\Elasticsearch\ClientBuilder; -use Magento\AdvancedSearch\Model\Client\ClientInterface; -use Magento\Elasticsearch\Model\Adapter\FieldsMappingPreprocessorInterface; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplatesProvider; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\LocalizedException; - -/** - * Elasticsearch client - */ -class Elasticsearch implements ClientInterface -{ - /** - * @var array - */ - private array $clientOptions; - - /** - * Elasticsearch Client instances - * - * @var Client[] - */ - private array $client; - - /** - * @var bool - */ - private bool $pingResult = false; - - /** - * @var FieldsMappingPreprocessorInterface[] - */ - private array $fieldsMappingPreprocessors; - - /** - * @var DynamicTemplatesProvider|null - */ - private $dynamicTemplatesProvider; - - /** - * Initialize Elasticsearch 8 Client - * - * @param array $options - * @param Client|null $elasticsearchClient - * @param array $fieldsMappingPreprocessors - * @param DynamicTemplatesProvider|null $dynamicTemplatesProvider - * @throws LocalizedException - */ - public function __construct( - array $options = [], - $elasticsearchClient = null, - array $fieldsMappingPreprocessors = [], - ?DynamicTemplatesProvider $dynamicTemplatesProvider = null - ) { - if (empty($options['hostname']) - || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) - && (empty($options['username']) || empty($options['password']))) - ) { - throw new LocalizedException( - __('The search failed because of a search engine misconfiguration.') - ); - } - // phpstan:ignore - if ($elasticsearchClient instanceof Client) { - $this->client[getmypid()] = $elasticsearchClient; - } - $this->clientOptions = $options; - $this->fieldsMappingPreprocessors = $fieldsMappingPreprocessors; - $this->dynamicTemplatesProvider = $dynamicTemplatesProvider ?: ObjectManager::getInstance() - ->get(DynamicTemplatesProvider::class); - } - - /** - * Get Elasticsearch 8 Client - * - * @return Client|null - */ - private function getElasticsearchClient(): ?Client /** @phpstan-ignore-line */ - { - // Intentionally added condition as there are BC changes from ES7 to ES8 - // and by default ES7 is configured. - if (!class_exists(\Elastic\Elasticsearch\Client::class)) { - return null; - } - - $pid = getmypid(); - if (!isset($this->client[$pid])) { - $config = $this->buildESConfig($this->clientOptions); - $this->client[$pid] = ClientBuilder::fromConfig($config, true); /** @phpstan-ignore-line */ - } - - return $this->client[$pid]; - } - - /** - * Ping the Elasticsearch 8 client - * - * @return bool - */ - public function ping(): bool - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($this->pingResult === false && $elasticsearchClient) { - $this->pingResult = $elasticsearchClient->ping( - ['client' => ['timeout' => $this->clientOptions['timeout']]] - )->asBool(); - } - - return $this->pingResult; - } - - /** - * Validate connection params for Elasticsearch 8 - * - * @return bool - */ - public function testConnection(): bool - { - return $this->ping(); - } - - /** - * Add/update an Elasticsearch index settings. - * - * @param string $index - * @param array $settings - * @return void - */ - public function putIndexSettings(string $index, array $settings): void - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $elasticsearchClient->indices() - ->putSettings(['index' => $index, 'body' => $settings]); - } - } - - /** - * Updates alias. - * - * @param string $alias - * @param string $newIndex - * @param string $oldIndex - * @return void - */ - public function updateAlias(string $alias, string $newIndex, string $oldIndex = '') - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient === null) { - return; - } - - $params = ['body' => ['actions' => []]]; - if ($newIndex) { - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; - } - - if ($oldIndex) { - $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; - } - - $elasticsearchClient->indices()->updateAliases($params); - } - - /** - * Checks whether Elasticsearch 8 index exists - * - * @param string $index - * @return bool - */ - public function indexExists(string $index): bool - { - $indexExists = false; - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $indexExists = $elasticsearchClient->indices() - ->exists(['index' => $index]) - ->asBool(); - } - - return $indexExists; - } - - /** - * Build config for Elasticsearch 8 - * - * @param array $options - * @return array - */ - private function buildESConfig(array $options = []): array - { - $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); - // @codingStandardsIgnoreStart - $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); - // @codingStandardsIgnoreEnd - if (!$protocol) { - $protocol = 'http'; - } - - $authString = ''; - if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) { - $authString = "{$options['username']}:{$options['password']}@"; - } - - $portString = ''; - if (!empty($options['port'])) { - $portString = ':' . $options['port']; - } - - $host = $protocol . '://' . $authString . $hostname . $portString; - - $options['hosts'] = [$host]; - - return $options; - } - - /** - * Exists alias. - * - * @param string $alias - * @param string $index - * @return bool - */ - public function existsAlias(string $alias, string $index = ''): bool - { - $existAlias = false; - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $params = ['name' => $alias]; - if ($index) { - $params['index'] = $index; - } - - $existAlias = $elasticsearchClient->indices()->existsAlias($params)->asBool(); - } - - return $existAlias; - } - - /** - * Performs bulk query over Elasticsearch 8 index - * - * @param array $query - * @return void - */ - public function bulkQuery(array $query) - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $elasticsearchClient->bulk($query); - } - } - - /** - * Creates an Elasticsearch 8 index. - * - * @param string $index - * @param array $settings - * @return void - */ - public function createIndex(string $index, array $settings): void - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $elasticsearchClient->indices() - ->create([ - 'index' => $index, - 'body' => $settings, - ]); - } - } - - /** - * Get alias. - * - * @param string $alias - * @return array - */ - public function getAlias(string $alias): array - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient === null) { - return []; - } - - return $elasticsearchClient->indices() - ->getAlias(['name' => $alias]) - ->asArray(); - } - - /** - * Add mapping to Elasticsearch 8 index - * - * @param array $fields - * @param string $index - * @param string $entityType - * @return void - * @SuppressWarnings("unused") - */ - public function addFieldsMapping(array $fields, string $index, string $entityType) - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient === null) { - return; - } - - $params = [ - 'index' => $index, - 'body' => [ - 'properties' => [], - 'dynamic_templates' => $this->dynamicTemplatesProvider->getTemplates(), - ], - ]; - - foreach ($this->applyFieldsMappingPreprocessors($fields) as $field => $fieldInfo) { - $params['body']['properties'][$field] = $fieldInfo; - } - - $elasticsearchClient->indices()->putMapping($params); - } - - /** - * Delete an Elasticsearch 8 index. - * - * @param string $index - * @return void - */ - public function deleteIndex(string $index) - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient) { - $elasticsearchClient->indices() - ->delete(['index' => $index]); - } - } - - /** - * Check if index is empty. - * - * @param string $index - * @return bool - */ - public function isEmptyIndex(string $index): bool - { - $elasticsearchClient = $this->getElasticsearchClient(); - if ($elasticsearchClient === null) { - return false; - } - - $stats = $this->getElasticsearchClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); - if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) { - return true; - } - - return false; - } - - /** - * Execute search by $query - * - * @param array $query - */ - public function query(array $query): array - { - $elasticsearchClient = $this->getElasticsearchClient(); - - return $elasticsearchClient === null ? [] : $elasticsearchClient->search($query)->asArray(); - } - - /** - * Get mapping from Elasticsearch index. - * - * @param array $params - * @return array - */ - public function getMapping(array $params): array - { - $elasticsearchClient = $this->getElasticsearchClient(); - - return $elasticsearchClient === null ? [] : $elasticsearchClient->indices()->getMapping($params)->asArray(); - } - - /** - * Apply fields mapping preprocessors - * - * @param array $properties - * @return array - */ - private function applyFieldsMappingPreprocessors(array $properties): array - { - foreach ($this->fieldsMappingPreprocessors as $preprocessor) { - $properties = $preprocessor->process($properties); - } - return $properties; - } -} diff --git a/app/code/Magento/Elasticsearch8/README.md b/app/code/Magento/Elasticsearch8/README.md deleted file mode 100644 index 25b26b4ec4373..0000000000000 --- a/app/code/Magento/Elasticsearch8/README.md +++ /dev/null @@ -1,32 +0,0 @@ -#Magento_Elasticsearch8 module - -Magento_Elasticsearch8 module allows using ElasticSearch engine 8.x version for the product searching capabilities. - -The module implements Magento_Search library interfaces. - -## Installation details - -The Magento_Elasticsearch8 module is one of the base Magento 2 modules. Disabling or uninstalling this module is not recommended. - -For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). - -## Structure - -`SearchAdapter/` - the directory that contains solutions for adapting ElasticSearch query searching. - -For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/). - -## Additional information - -By default`indices.id_field_data` is disallowed in Elasticsearch8 hence it needs to enabled it from `elasticsearch.yml` -by adding the following configuration -`indices: -id_field_data: -enabled: true` - -More information about ElasticSearch are at articles: - -- [Configuring Catalog Search](https://experienceleague.adobe.com/docs/commerce-admin/catalog/catalog/search/search-configuration.html). -- [Installation Guide/Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/search-engine/overview.html). -- [Configure and maintain Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/search/overview-search.html). -- Magento Commerce Cloud - [set up Elasticsearch service](https://experienceleague.adobe.com/docs/commerce-cloud-service/user-guide/configure/service/elasticsearch.html). diff --git a/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php deleted file mode 100644 index 2d159c764bf69..0000000000000 --- a/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php +++ /dev/null @@ -1,125 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\SearchAdapter; - -use Magento\Elasticsearch\SearchAdapter\Aggregation\Builder as AggregationBuilder; -use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\SearchAdapter\QueryContainerFactory; -use Magento\Elasticsearch\SearchAdapter\ResponseFactory; -use Magento\Framework\Search\AdapterInterface; -use Magento\Framework\Search\RequestInterface; -use Magento\Framework\Search\Response\QueryResponse; -use Psr\Log\LoggerInterface; - -/** - * Elasticsearch Search Adapter - */ -class Adapter implements AdapterInterface -{ - /** - * Mapper instance - * - * @var Mapper - */ - private Mapper $mapper; - - /** - * @var ResponseFactory - */ - private ResponseFactory $responseFactory; - - /** - * @var ConnectionManager - */ - private ConnectionManager $connectionManager; - - /** - * @var AggregationBuilder - */ - private AggregationBuilder $aggregationBuilder; - - /** - * @var QueryContainerFactory - */ - private QueryContainerFactory $queryContainerFactory; - - /** - * Empty response from Elasticsearch - * - * @var array - */ - private static array $emptyRawResponse = [ - "hits" => [ - "hits" => [] - ], - "aggregations" => [ - "price_bucket" => [], - "category_bucket" => ["buckets" => []], - ] - ]; - - /** - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * @param ConnectionManager $connectionManager - * @param Mapper $mapper - * @param ResponseFactory $responseFactory - * @param AggregationBuilder $aggregationBuilder - * @param QueryContainerFactory $queryContainerFactory - * @param LoggerInterface $logger - */ - public function __construct( - ConnectionManager $connectionManager, - Mapper $mapper, - ResponseFactory $responseFactory, - AggregationBuilder $aggregationBuilder, - QueryContainerFactory $queryContainerFactory, - LoggerInterface $logger - ) { - $this->connectionManager = $connectionManager; - $this->mapper = $mapper; - $this->responseFactory = $responseFactory; - $this->aggregationBuilder = $aggregationBuilder; - $this->queryContainerFactory = $queryContainerFactory; - $this->logger = $logger; - } - - /** - * Search query - * - * @param RequestInterface $request - * @return QueryResponse - */ - public function query(RequestInterface $request) : QueryResponse - { - $client = $this->connectionManager->getConnection(); - $query = $this->mapper->buildQuery($request); - $aggregationBuilder = $this->aggregationBuilder; - $aggregationBuilder->setQuery($this->queryContainerFactory->create(['query' => $query])); - - try { - $rawResponse = $client->query($query); - } catch (\Exception $e) { - $this->logger->critical($e); - // return empty search result in case an exception is thrown from Elasticsearch - $rawResponse = self::$emptyRawResponse; - } - - $rawDocuments = $rawResponse['hits']['hits'] ?? []; - return $this->responseFactory->create( - [ - 'documents' => $rawDocuments, - 'aggregations' => $aggregationBuilder->build($request, $rawResponse), - 'total' => $rawResponse['hits']['total']['value'] ?? 0 - ] - ); - } -} diff --git a/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php deleted file mode 100644 index 8c320ed152dba..0000000000000 --- a/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Elasticsearch8\SearchAdapter; - -use Magento\Framework\Search\RequestInterface; -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper as Elasticsearch5Mapper; - -/** - * Elasticsearch8 mapper class - */ -class Mapper -{ - /** - * @var Elasticsearch5Mapper - */ - private Elasticsearch5Mapper $mapper; - - /** - * Mapper constructor. - * @param Elasticsearch5Mapper $mapper - */ - public function __construct(Elasticsearch5Mapper $mapper) - { - $this->mapper = $mapper; - } - - /** - * Build adapter dependent query - * - * @param RequestInterface $request - * @return array - */ - public function buildQuery(RequestInterface $request) : array - { - $searchQuery = $this->mapper->buildQuery($request); - $searchQuery['track_total_hits'] = true; - return $searchQuery; - } -} diff --git a/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml b/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml deleted file mode 100644 index cf733bd22f8e4..0000000000000 --- a/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml +++ /dev/null @@ -1,63 +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="StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest"> - <annotations> - <features value="Elasticsearch8"/> - <stories value="Storefront Search"/> - <title value="Check that AND query is performed when searching using ElasticSearch 8"/> - <description value="Check that AND query is performed when searching using ElasticSearch 8"/> - <severity value="CRITICAL"/> - <testCaseId value="AC-6597"/> - <useCaseId value="AC-6665"/> - <group value="SearchEngine"/> - <group value="pr_exclude"/> - </annotations> - <before> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="setSearchEngine"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProducts"/> - <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="createFirtsSimpleProduct"/> - <createData entity="SimpleProductWithCustomSku24MB06" stepKey="createSecondSimpleProduct"/> - <createData entity="SimpleProductWithCustomSku24MB04" stepKey="createThirdSimpleProduct"/> - <createData entity="SimpleProductWithCustomSku24MB02" stepKey="createFourthSimpleProduct"/> - <createData entity="SimpleProductWithCustomSku24MB01" stepKey="createFifthSimpleProduct"/> - <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> - <argument name="tags" value="config full_page"/> - </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> - </before> - <after> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="deleteProductOne"/> - <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProductsAfterTest"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> - </after> - <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> - <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductSku"> - <argument name="phrase" value="24 MB04"/> - </actionGroup> - - <see userInput="4" selector="{{StorefrontCatalogSearchMainSection.productCount}}" stepKey="assertSearchResultCount"/> - - <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertSecondProductName"> - <argument name="productName" value="$createSecondSimpleProduct.name$"/> - </actionGroup> - <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertThirdProductName"> - <argument name="productName" value="$createThirdSimpleProduct.name$"/> - </actionGroup> - <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertFourthProductName"> - <argument name="productName" value="$createFourthSimpleProduct.name$"/> - </actionGroup> - <actionGroup ref="StorefrontQuickSearchSeeProductByNameActionGroup" stepKey="assertFifthProductName"> - <argument name="productName" value="$createFifthSimpleProduct.name$"/> - </actionGroup> - </test> -</tests> diff --git a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php deleted file mode 100644 index 1598c83c735d5..0000000000000 --- a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; - -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver - as BaseDefaultResolver; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface - as FieldTypeConverterInterface; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface - as FieldTypeResolver; -use Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD) - */ -class DefaultResolverTest extends TestCase -{ - /** - * @var DefaultResolver - */ - private $resolver; - - /** - * @var FieldTypeResolver - */ - private $fieldTypeResolver; - - /** - * @var FieldTypeConverterInterface - */ - private $fieldTypeConverter; - - /** - * Set up test environment - * - * @return void - */ - protected function setUp(): void - { - $objectManager = new ObjectManagerHelper($this); - $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) - ->disableOriginalConstructor() - ->setMethods(['convert']) - ->getMockForAbstractClass(); - $this->fieldTypeResolver = $this->getMockBuilder(FieldTypeResolver::class) - ->disableOriginalConstructor() - ->setMethods(['getFieldType']) - ->getMockForAbstractClass(); - - $baseResolver = $objectManager->getObject( - BaseDefaultResolver::class, - [ - 'fieldTypeResolver' => $this->fieldTypeResolver, - 'fieldTypeConverter' => $this->fieldTypeConverter - ] - ); - - $this->resolver = $objectManager->getObject(DefaultResolver::class, ['baseResolver' => $baseResolver]); - } - - /** - * @dataProvider getFieldNameProvider - * @param $fieldType - * @param $attributeCode - * @param $frontendInput - * @param $isSortable - * @param $context - * @param $expected - * @return void - */ - public function testGetFieldName( - $fieldType, - $attributeCode, - $frontendInput, - $isSortable, - $context, - $expected - ) { - $attributeMock = $this->getMockBuilder(AttributeAdapter::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) - ->getMock(); - $this->fieldTypeConverter->expects($this->any()) - ->method('convert') - ->willReturn('string'); - $attributeMock->expects($this->any()) - ->method('getFrontendInput') - ->willReturn($frontendInput); - $attributeMock->expects($this->any()) - ->method('getAttributeCode') - ->willReturn($attributeCode); - $attributeMock->expects($this->any()) - ->method('isSortable') - ->willReturn($isSortable); - $this->fieldTypeResolver->expects($this->any()) - ->method('getFieldType') - ->willReturn($fieldType); - - $this->assertEquals( - $expected, - $this->resolver->getFieldName($attributeMock, $context) - ); - } - - /** - * @return array - */ - public function getFieldNameProvider(): array - { - return [ - ['', 'code', '', false, [], 'code'], - ['', 'code', '', false, ['type' => 'default'], 'code'], - ['string', '*', '', false, ['type' => 'default'], '_search'], - ['', 'code', '', false, ['type' => 'default'], 'code'], - ['', 'code', 'select', false, ['type' => 'default'], 'code'], - ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], - ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], - ]; - } -} diff --git a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php deleted file mode 100644 index f27235d30229b..0000000000000 --- a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php +++ /dev/null @@ -1,680 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Elasticsearch8\Test\Unit\Model\Client; - -use DG\BypassFinals; -use Elastic\Elasticsearch\Client; -use Elastic\Elasticsearch\Endpoints\Indices; -use Elastic\Elasticsearch\Response\Elasticsearch as ElasticsearchResponse; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\AddDefaultSearchField; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\IntegerMapper; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PositionMapper; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PriceMapper; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\StringMapper; -use Magento\Elasticsearch8\Model\Adapter\DynamicTemplatesProvider; -use Magento\Elasticsearch8\Model\Client\Elasticsearch; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Class ElasticsearchTest to test Elasticsearch 8 - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ElasticsearchTest extends TestCase -{ - /** - * @var Elasticsearch - */ - private $model; - - /** - * @var Client|MockObject - */ - private $elasticsearchClientMock; - - /** - * @var Indices|MockObject - */ - private $indicesMock; - - /** - * @var ObjectManagerHelper - */ - private $objectManager; - - /** @var ElasticsearchResponse|MockObject */ - private $elasticsearchResponse; - - /** - * Setup - * - * @return void - */ - protected function setUp(): void - { - BypassFinals::enable(); - $this->elasticsearchClientMock = $this->getMockBuilder(Client::class) /** @phpstan-ignore-line */ - ->setMethods( - [ - 'indices', - 'ping', - 'bulk', - 'search', - 'scroll', - 'info', - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->indicesMock = $this->getMockBuilder(Indices::class) /** @phpstan-ignore-line */ - ->setMethods( - [ - 'exists', - 'getSettings', - 'create', - 'delete', - 'putMapping', - 'deleteMapping', - 'getMapping', - 'stats', - 'updateAliases', - 'existsAlias', - 'getAlias', - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->elasticsearchResponse = $this->getMockBuilder(ElasticsearchResponse::class) /** @phpstan-ignore-line */ - ->setMethods([ - 'asBool', - 'asArray', - ]) - ->getMock(); - $this->elasticsearchClientMock->expects($this->any()) - ->method('indices') - ->willReturn($this->indicesMock); - $this->elasticsearchClientMock->expects($this->any()) - ->method('ping') - ->willReturn($this->elasticsearchResponse); - - $this->objectManager = new ObjectManagerHelper($this); - $dynamicTemplatesProvider = new DynamicTemplatesProvider( - [ - new PriceMapper(), - new PositionMapper(), - new StringMapper(), - new IntegerMapper(), - ] - ); - $this->model = $this->objectManager->getObject( - Elasticsearch::class, - [ - 'options' => $this->getOptions(), - 'elasticsearchClient' => $this->elasticsearchClientMock, - 'fieldsMappingPreprocessors' => [new AddDefaultSearchField()], - 'dynamicTemplatesProvider' => $dynamicTemplatesProvider, - ] - ); - } - - /** - * Test configurations with exception - * - * @return void - */ - public function testConstructorOptionsException() - { - $this->expectException('Magento\Framework\Exception\LocalizedException'); - $result = $this->objectManager->getObject( - Elasticsearch::class, - [ - 'options' => [], - ] - ); - $this->assertNotNull($result); - } - - /** - * Test client creation from the list of options - */ - public function testConstructorWithOptions() - { - $result = $this->objectManager->getObject( - Elasticsearch::class, - [ - 'options' => $this->getOptions(), - ] - ); - $this->assertNotNull($result); - } - - /** - * Ensure that configuration returns correct url. - * - * @param array $options - * @param string $expectedResult - * @throws LocalizedException - * @throws \ReflectionException - * @dataProvider getOptionsDataProvider - */ - public function testBuildConfig(array $options, string $expectedResult): void - { - $buildConfig = new Elasticsearch($options); - $config = $this->getPrivateMethod(); - $result = $config->invoke($buildConfig, $options); - $this->assertEquals($expectedResult, $result['hosts'][0]); - } - - /** - * Return private method for elastic search class. - * - * @return \ReflectionMethod - */ - private function getPrivateMethod(): \ReflectionMethod - { - $reflector = new \ReflectionClass(Elasticsearch::class); - $method = $reflector->getMethod('buildESConfig'); - $method->setAccessible(true); - - return $method; - } - - /** - * Get options data provider. - */ - public function getOptionsDataProvider(): array - { - return [ - [ - 'without_protocol' => [ - 'hostname' => 'localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 0, - ], - 'expected_result' => 'http://localhost:9200', - ], - [ - 'with_protocol' => [ - 'hostname' => 'https://localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 0, - ], - 'expected_result' => 'https://localhost:9200', - ], - ]; - } - - /** - * Test ping functionality - */ - public function testPing() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $this->assertTrue($this->model->ping()); - } - - /** - * Get elasticsearch client options - * - * @return array - */ - protected function getOptions(): array - { - return [ - 'hostname' => 'localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 1, - 'username' => 'user', - 'password' => 'passwd', - ]; - } - - /** - * Test validation of connection parameters - */ - public function testTestConnection() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $this->assertTrue($this->model->testConnection()); - } - - /** - * Test validation of connection parameters returns false - */ - public function testTestConnectionFalse() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(false); - $this->assertFalse($this->model->testConnection()); - } - - /** - * Test validation of connection parameters - */ - public function testTestConnectionPing() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $this->model = $this->objectManager->getObject( - Elasticsearch::class, - [ - 'options' => $this->getEmptyIndexOption(), - 'elasticsearchClient' => $this->elasticsearchClientMock, - ] - ); - - $this->model->ping(); - $this->assertTrue($this->model->testConnection()); - } - - /** - * @return array - */ - private function getEmptyIndexOption(): array - { - return [ - 'hostname' => 'localhost', - 'port' => '9200', - 'index' => '', - 'timeout' => 15, - 'enableAuth' => 1, - 'username' => 'user', - 'password' => 'passwd', - ]; - } - - /** - * Test bulkQuery() method - */ - public function testBulkQuery() - { - $this->elasticsearchClientMock->expects($this->once()) - ->method('bulk') - ->with([]); - $this->model->bulkQuery([]); - } - - /** - * Test createIndex() method, case when such index exists - */ - public function testCreateIndexExists() - { - $this->indicesMock->expects($this->once()) - ->method('create') - ->with( - [ - 'index' => 'indexName', - 'body' => [], - ] - ); - $this->model->createIndex('indexName', []); - } - - /** - * Test deleteIndex() method. - */ - public function testDeleteIndex() - { - $this->indicesMock->expects($this->once()) - ->method('delete') - ->with(['index' => 'indexName']); - $this->model->deleteIndex('indexName'); - } - - /** - * Test isEmptyIndex() method. - */ - public function testIsEmptyIndex() - { - $indexName = 'magento2_test_index'; - $stats['indices'][$indexName]['primaries']['docs']['count'] = 0; - - $this->indicesMock->expects($this->once()) - ->method('stats') - ->with(['index' => $indexName, 'metric' => 'docs']) - ->willReturn($stats); - $this->assertTrue($this->model->isEmptyIndex($indexName)); - } - - /** - * Test isEmptyIndex() method returns false. - */ - public function testIsEmptyIndexFalse() - { - $indexName = 'magento2_test_index'; - $stats['indices'][$indexName]['primaries']['docs']['count'] = 1; - - $this->indicesMock->expects($this->once()) - ->method('stats') - ->with(['index' => $indexName, 'metric' => 'docs']) - ->willReturn($stats); - $this->assertFalse($this->model->isEmptyIndex($indexName)); - } - - /** - * Test updateAlias() method with new index. - */ - public function testUpdateAlias() - { - $alias = 'alias1'; - $index = 'index1'; - - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $index]]; - - $this->indicesMock->expects($this->once()) - ->method('updateAliases') - ->with($params); - $this->model->updateAlias($alias, $index); - } - - /** - * Test updateAlias() method with new and old index. - */ - public function testUpdateAliasRemoveOldIndex() - { - $alias = 'alias1'; - $newIndex = 'index1'; - $oldIndex = 'indexOld'; - - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; - $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; - - $this->indicesMock->expects($this->once()) - ->method('updateAliases') - ->with($params); - $this->model->updateAlias($alias, $newIndex, $oldIndex); - } - - /** - * Test indexExists() method, case when no such index exists - */ - public function testIndexExists() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $this->indicesMock->expects($this->once()) - ->method('exists') - ->with(['index' => 'indexName']) - ->willReturn($this->elasticsearchResponse); - $this->model->indexExists('indexName'); - } - - /** - * Tests existsAlias() method checking for alias. - */ - public function testExistsAlias() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $alias = 'alias1'; - $params = ['name' => $alias]; - $this->indicesMock->expects($this->once()) - ->method('existsAlias') - ->with($params) - ->willReturn($this->elasticsearchResponse); - $this->assertTrue($this->model->existsAlias($alias)); - } - - /** - * Tests existsAlias() method checking for alias and index. - */ - public function testExistsAliasWithIndex() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asBool') - ->willReturn(true); - $alias = 'alias1'; - $index = 'index1'; - $params = ['name' => $alias, 'index' => $index]; - $this->indicesMock->expects($this->once()) - ->method('existsAlias') - ->with($params) - ->willReturn($this->elasticsearchResponse); - $this->assertTrue($this->model->existsAlias($alias, $index)); - } - - /** - * Test getAlias() method. - */ - public function testGetAlias() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asArray') - ->willReturn([]); - $alias = 'alias1'; - $params = ['name' => $alias]; - $this->indicesMock->expects($this->once()) - ->method('getAlias') - ->with($params) - ->willReturn($this->elasticsearchResponse); - $this->assertEquals([], $this->model->getAlias($alias)); - } - - /** - * Test createIndexIfNotExists() method, case when operation fails - */ - public function testCreateIndexFailure() - { - $this->expectException('Exception'); - $this->indicesMock->expects($this->once()) - ->method('create') - ->with( - [ - 'index' => 'indexName', - 'body' => [], - ] - ) - ->willThrowException(new \Exception('Something went wrong')); - $this->model->createIndex('indexName', []); - } - - /** - * Test testAddFieldsMapping() method - */ - public function testAddFieldsMapping() - { - $this->indicesMock->expects($this->once()) - ->method('putMapping') - ->with( - [ - 'index' => 'indexName', - 'body' => [ - 'properties' => [ - '_search' => [ - 'type' => 'text', - ], - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => true, - 'copy_to' => '_search', - ], - ], - ], - [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ], - ], - ], - ] - ); - $this->model->addFieldsMapping( - [ - 'name' => [ - 'type' => 'text', - ], - ], - 'indexName', - 'product' - ); - } - - /** - * Test testAddFieldsMapping() method - */ - public function testAddFieldsMappingFailure() - { - $this->expectException('Exception'); - $this->indicesMock->expects($this->once()) - ->method('putMapping') - ->with( - [ - 'index' => 'indexName', - 'body' => [ - 'properties' => [ - '_search' => [ - 'type' => 'text', - ], - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => true, - 'copy_to' => '_search', - ], - ], - ], - [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ], - ], - ], - ] - ) - ->willThrowException(new \Exception('Something went wrong')); - $this->model->addFieldsMapping( - [ - 'name' => [ - 'type' => 'text', - ], - ], - 'indexName', - 'product' - ); - } - - /** - * Test get Elasticsearch mapping process. - * - * @return void - */ - public function testGetMapping(): void - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asArray') - ->willReturn([]); - $params = ['index' => 'indexName']; - $this->indicesMock->expects($this->once()) - ->method('getMapping') - ->with($params) - ->willReturn($this->elasticsearchResponse); - - $this->model->getMapping($params); - } - - /** - * Test query() method - * - * @return void - */ - public function testQuery() - { - $this->elasticsearchResponse->expects($this->once()) - ->method('asArray') - ->willReturn([]); - $query = ['test phrase query']; - $this->elasticsearchClientMock->expects($this->once()) - ->method('search') - ->with($query) - ->willReturn($this->elasticsearchResponse); - $this->assertEquals([], $this->model->query($query)); - } -} diff --git a/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml deleted file mode 100644 index 590f717d306b4..0000000000000 --- a/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml +++ /dev/null @@ -1,93 +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:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="catalog"> - <group id="search"> - <!-- Elasticsearch 8 --> - <field id="elasticsearch8_server_hostname" translate="label" type="text" sortOrder="61" - showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Elasticsearch Server Hostname</label> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - - <field id="elasticsearch8_server_port" translate="label" type="text" sortOrder="62" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Elasticsearch Server Port</label> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - - <field id="elasticsearch8_index_prefix" translate="label" type="text" sortOrder="63" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Elasticsearch Index Prefix</label> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - - <field id="elasticsearch8_enable_auth" translate="label" type="select" sortOrder="64" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Enable Elasticsearch HTTP Auth</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - - <field id="elasticsearch8_username" translate="label" type="text" sortOrder="65" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Elasticsearch HTTP Username</label> - <depends> - <field id="engine">elasticsearch8</field> - <field id="elasticsearch8_enable_auth">1</field> - </depends> - </field> - - <field id="elasticsearch8_password" translate="label" type="text" sortOrder="66" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Elasticsearch HTTP Password</label> - <depends> - <field id="engine">elasticsearch8</field> - <field id="elasticsearch8_enable_auth">1</field> - </depends> - </field> - - <field id="elasticsearch8_server_timeout" translate="label" type="text" sortOrder="67" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label>Elasticsearch Server Timeout</label> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - - <field id="elasticsearch8_test_connect_wizard" translate="button_label" sortOrder="68" showInDefault="1" - showInWebsite="0" showInStore="0"> - <label/> - <button_label>Test Connection</button_label> - <frontend_model>Magento\Elasticsearch8\Block\Adminhtml\System\Config\TestConnection</frontend_model> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - </field> - <field id="elasticsearch8_minimum_should_match" translate="label" type="text" sortOrder="93" showInDefault="1"> - <label>Minimum Terms to Match</label> - <depends> - <field id="engine">elasticsearch8</field> - </depends> - <comment><![CDATA[<a href="https://experienceleague.adobe.com/docs/commerce-admin/catalog/catalog/search/search-configuration.html">Learn more</a> about valid syntax.]]></comment> - <backend_model>Magento\Elasticsearch\Model\Config\Backend\MinimumShouldMatch</backend_model> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/Elasticsearch8/etc/config.xml b/app/code/Magento/Elasticsearch8/etc/config.xml deleted file mode 100644 index 016b249abd4db..0000000000000 --- a/app/code/Magento/Elasticsearch8/etc/config.xml +++ /dev/null @@ -1,21 +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:module:Magento_Store:etc/config.xsd"> - <default> - <catalog> - <search> - <elasticsearch8_server_hostname>localhost</elasticsearch8_server_hostname> - <elasticsearch8_server_port>9200</elasticsearch8_server_port> - <elasticsearch8_index_prefix>magento2</elasticsearch8_index_prefix> - <elasticsearch8_enable_auth>0</elasticsearch8_enable_auth> - <elasticsearch8_server_timeout>15</elasticsearch8_server_timeout> - <elasticsearch8_minimum_should_match/> - </search> - </catalog> - </default> -</config> diff --git a/app/code/Magento/Elasticsearch8/etc/di.xml b/app/code/Magento/Elasticsearch8/etc/di.xml deleted file mode 100644 index c73462a8da22f..0000000000000 --- a/app/code/Magento/Elasticsearch8/etc/di.xml +++ /dev/null @@ -1,281 +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\Elasticsearch\Model\Adapter\Elasticsearch" type="Magento\Elasticsearch8\Model\Adapter\Elasticsearch"/> - <type name="Magento\Elasticsearch\Model\Config"> - <arguments> - <argument name="engineList" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">elasticsearch8</item> - </argument> - </arguments> - </type> - - <type name="Magento\Elasticsearch\Model\Adapter\Elasticsearch"> - <arguments> - <argument name="responseErrorExceptionList" xsi:type="array"> - <item name="clientResponseException" xsi:type="string">Elastic\Elasticsearch\Exception\ClientResponseException</item> - </argument> - </arguments> - </type> - - <type name="Magento\Elasticsearch\Model\DataProvider\Base\Suggestions"> - <arguments> - <argument name="responseErrorExceptionList" xsi:type="array"> - <item name="clientResponseException" xsi:type="string">Elastic\Elasticsearch\Exception\ClientResponseException</item> - </argument> - </arguments> - </type> - - <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> - <arguments> - <argument name="engines" xsi:type="array"> - <item sortOrder="30" name="elasticsearch8" xsi:type="string">Elasticsearch 8</item> - </argument> - </arguments> - </type> - - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> - <arguments> - <argument name="categoryFieldsProviders" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> - <arguments> - <argument name="productFieldMappers" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch8\Model\Adapter\FieldMapper\ProductFieldMapper</item> - </argument> - </arguments> - </type> - - <type name="Magento\AdvancedSearch\Model\Client\ClientResolver"> - <arguments> - <argument name="clientFactories" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">\Magento\Elasticsearch8\Model\Client\ElasticsearchFactory</item> - </argument> - <argument name="clientOptions" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">\Magento\Elasticsearch\Model\Config</item> - </argument> - </arguments> - </type> - - <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> - <arguments> - <argument name="handlers" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexerHandler</item> - </argument> - </arguments> - </type> - - <type name="Magento\CatalogSearch\Model\Indexer\IndexStructureFactory"> - <arguments> - <argument name="structures" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexStructure</item> - </argument> - </arguments> - </type> - - <type name="Magento\CatalogSearch\Model\ResourceModel\EngineProvider"> - <arguments> - <argument name="engines" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Engine</item> - </argument> - </arguments> - </type> - - <type name="Magento\Search\Model\AdapterFactory"> - <arguments> - <argument name="adapters" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">\Magento\Elasticsearch8\SearchAdapter\Adapter</item> - </argument> - </arguments> - </type> - - <type name="Magento\Search\Model\EngineResolver"> - <arguments> - <argument name="engines" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">elasticsearch8</item> - </argument> - <argument name="defaultEngine" xsi:type="string">elasticsearch8</argument> - </arguments> - </type> - - <virtualType name="Magento\Elasticsearch8\Model\Client\ElasticsearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> - <arguments> - <argument name="clientClass" xsi:type="string">Magento\Elasticsearch8\Model\Client\Elasticsearch</argument> - </arguments> - </virtualType> - - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> - <arguments> - <argument name="clientFactories" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch8\Model\Client\ElasticsearchFactory</item> - </argument> - </arguments> - </type> - - <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> - <arguments> - <argument name="intervals" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> - </argument> - </arguments> - </type> - - <type name="Magento\Framework\Search\Dynamic\DataProviderFactory"> - <arguments> - <argument name="dataProviders" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider</item> - </argument> - </arguments> - </type> - - <virtualType name="Magento\Elasticsearch8\Model\DataProvider\Suggestions" type="Magento\Elasticsearch\Model\DataProvider\Base\Suggestions"> - <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> - </arguments> - </virtualType> - <type name="Magento\AdvancedSearch\Model\SuggestedQueries"> - <arguments> - <argument name="data" xsi:type="array"> - <item name="elasticsearch8" xsi:type="string">Magento\Elasticsearch8\Model\DataProvider\Suggestions</item> - </argument> - </arguments> - </type> - <virtualType name="\Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> - <arguments> - <argument name="items" xsi:type="array"> - <item name="notEav" xsi:type="object" sortOrder="10">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> - <item name="special" xsi:type="object" sortOrder="20">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\SpecialAttribute</item> - <item name="price" xsi:type="object" sortOrder="30">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price</item> - <item name="categoryName" xsi:type="object" sortOrder="40">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName</item> - <item name="position" xsi:type="object" sortOrder="50">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position</item> - <item name="default" xsi:type="object" sortOrder="100">Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver</item> - </argument> - </arguments> - </virtualType> - <type name="Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver"> - <arguments> - <argument name="baseResolver" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver</argument> - </arguments> - </type> - <virtualType name="Magento\Elasticsearch8\Model\Adapter\FieldMapper\ProductFieldMapper" - type="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> - <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> - <argument name="fieldNameResolver" xsi:type="object">\Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver</argument> - </arguments> - </virtualType> - - <type name="Magento\Search\Model\Search\PageSizeProvider"> - <arguments> - <argument name="pageSizeBySearchEngine" xsi:type="array"> - <item name="elasticsearch8" xsi:type="number">10000</item> - </argument> - </arguments> - </type> - - <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> - <arguments> - <argument name="factories" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">elasticsearchCategoryCollectionFactory</item> - </argument> - </arguments> - </virtualType> - - <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> - <arguments> - <argument name="factories" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> - <item name="default" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> - </argument> - </arguments> - </type> - - <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> - <arguments> - <argument name="strategies" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> - </argument> - </arguments> - </type> - - <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> - <arguments> - <argument name="factories" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> - </argument> - </arguments> - </virtualType> - - <type name="Magento\Config\Model\Config\TypePool"> - <arguments> - <argument name="sensitive" xsi:type="array"> - <item name="catalog/search/elasticsearch8_password" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_server_hostname" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_username" xsi:type="string">1</item> - </argument> - <argument name="environment" xsi:type="array"> - <item name="catalog/search/elasticsearch8_enable_auth" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_index_prefix" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_password" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_server_hostname" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_server_port" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_username" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch8_server_timeout" xsi:type="string">1</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch8\Model\Client\Elasticsearch"> - <arguments> - <argument name="fieldsMappingPreprocessors" xsi:type="array"> - <item name="elasticsearch8_copy_searchable_fields_to_search_field" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\CopySearchableFieldsToSearchField</item> - <item name="elasticsearch8_add_default_search_field" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\AddDefaultSearchField</item> - </argument> - </arguments> - </type> - - <virtualType name="Magento\Elasticsearch8\Setup\InstallConfig" type="Magento\Search\Setup\InstallConfig"> - <arguments> - <argument name="searchConfigMapping" xsi:type="array"> - <item name="elasticsearch-host" xsi:type="string">elasticsearch8_server_hostname</item> - <item name="elasticsearch-port" xsi:type="string">elasticsearch8_server_port</item> - <item name="elasticsearch-timeout" xsi:type="string">elasticsearch8_server_timeout</item> - <item name="elasticsearch-index-prefix" xsi:type="string">elasticsearch8_index_prefix</item> - <item name="elasticsearch-enable-auth" xsi:type="string">elasticsearch8_enable_auth</item> - <item name="elasticsearch-username" xsi:type="string">elasticsearch8_username</item> - <item name="elasticsearch-password" xsi:type="string">elasticsearch8_password</item> - </argument> - </arguments> - </virtualType> - <type name="Magento\Search\Setup\CompositeInstallConfig"> - <arguments> - <argument name="installConfigList" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch8\Setup\InstallConfig</item> - </argument> - </arguments> - </type> - <type name="Magento\Search\Model\SearchEngine\Validator"> - <arguments> - <argument name="engineValidators" xsi:type="array"> - <item name="elasticsearch8" xsi:type="object">Magento\Elasticsearch\Setup\Validator</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch8\Model\Adapter\DynamicTemplatesProvider"> - <arguments> - <argument name="mappers" xsi:type="array"> - <item name="price_mapping" xsi:type="object">Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PriceMapper</item> - <item name="position_mapping" xsi:type="object">Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PositionMapper</item> - <item name="string_mapping" xsi:type="object">Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\StringMapper</item> - <item name="integer_mapping" xsi:type="object">Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\IntegerMapper</item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/Elasticsearch8/etc/search_engine.xml b/app/code/Magento/Elasticsearch8/etc/search_engine.xml deleted file mode 100644 index 28e4074bfc886..0000000000000 --- a/app/code/Magento/Elasticsearch8/etc/search_engine.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<engines xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Search/etc/search_engine.xsd"> - <engine name="elasticsearch8"> - <feature name="synonyms" support="true" /> - </engine> -</engines> diff --git a/app/code/Magento/Elasticsearch8/registration.php b/app/code/Magento/Elasticsearch8/registration.php deleted file mode 100644 index bfe52f2f4ceee..0000000000000 --- a/app/code/Magento/Elasticsearch8/registration.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -\Magento\Framework\Component\ComponentRegistrar::register( - \Magento\Framework\Component\ComponentRegistrar::MODULE, - 'Magento_Elasticsearch8', - __DIR__ -); diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 4b74ab71b3419..c4f6784aaa79e 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -81,6 +81,11 @@ class Filter extends Template */ protected $_modifiers = ['nl2br' => '']; + /** + * @var string + */ + private const CACHE_KEY_PREFIX = "EMAIL_FILTER_"; + /** * @var bool */ @@ -284,7 +289,7 @@ public function setUseAbsoluteLinks($flag) * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @deprecated SID query parameter is not used in URLs anymore. - * @see setUseSessionInUrl + * @see SessionId's in URL */ public function setUseSessionInUrl($flag) { @@ -408,6 +413,11 @@ public function blockDirective($construction) { $skipParams = ['class', 'id', 'output']; $blockParameters = $this->getParameters($construction[2]); + + if (isset($blockParameters['cache_key'])) { + $blockParameters['cache_key'] = self::CACHE_KEY_PREFIX . $blockParameters['cache_key']; + } + $block = null; if (isset($blockParameters['class'])) { @@ -694,7 +704,7 @@ public function varDirective($construction) * @param string $default assumed modifier if none present * @return array * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces - * @see explodeModifiers + * @see Directive Processor Interfaces */ protected function explodeModifiers($value, $default = null) { @@ -714,7 +724,7 @@ protected function explodeModifiers($value, $default = null) * @param string $modifiers * @return string * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces - * @see applyModifiers + * @see Directive Processor Interfaces */ protected function applyModifiers($value, $modifiers) { @@ -744,7 +754,7 @@ protected function applyModifiers($value, $modifiers) * @param string $type * @return string * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces - * @see modifierEscape + * @see Directive Processor Interfacees */ public function modifierEscape($value, $type = 'html') { @@ -1124,16 +1134,16 @@ public function filter($value) try { $value = parent::filter($value); } catch (Exception $e) { - // Since a single instance of this class can be used to filter content multiple times, reset callbacks to - // prevent callbacks running for unrelated content (e.g., email subject and email body) - $this->resetAfterFilterCallbacks(); - if ($this->_appState->getMode() == State::MODE_DEVELOPER) { $value = sprintf(__('Error filtering template: %s')->render(), $e->getMessage()); } else { $value = (string) __("We're sorry, an error has occurred while generating this content."); } $this->_logger->critical($e); + } finally { + // Since a single instance of this class can be used to filter content multiple times, reset callbacks to + // prevent callbacks running for unrelated content (e.g., email subject and email body) + $this->resetAfterFilterCallbacks(); } return $value; } diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml index 40f7b48b21122..2487c288af115 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml index 89b07e4be44e9..a2d22a14acd2e 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-10932"/> <group value="theme"/> <group value="email"/> + <group value="cloud"/> </annotations> <before> <!--Login to Admin Area--> diff --git a/app/code/Magento/Email/etc/config.xml b/app/code/Magento/Email/etc/config.xml index 6f486c15472c2..88f7b81ea2ea8 100644 --- a/app/code/Magento/Email/etc/config.xml +++ b/app/code/Magento/Email/etc/config.xml @@ -27,6 +27,7 @@ <disable>0</disable> <host>localhost</host> <port>25</port> + <password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <set_return_path>0</set_return_path> <transport>sendmail</transport> <auth>none</auth> diff --git a/app/code/Magento/EncryptionKey/README.md b/app/code/Magento/EncryptionKey/README.md index 07838cceeb3f2..1d4f642ac6033 100644 --- a/app/code/Magento/EncryptionKey/README.md +++ b/app/code/Magento/EncryptionKey/README.md @@ -1,4 +1,4 @@ -#Magento_EncryptionKey module +# Magento_EncryptionKey module The Magento_EncryptionKey module provides an advanced encryption model to protect passwords and other sensitive data. diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml index 02e94d0410103..46b0af6a41ba8 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="encryption_key"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml index 10787d056a187..3aa71154de419 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="encryption_key"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php new file mode 100644 index 0000000000000..33cd6f64de9cf --- /dev/null +++ b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Fedex\Model\Config\Backend; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\Validator\Url; + +/** + * Represents a config URL that may point to a Fedex endpoint + */ +class FedexUrl extends Value +{ + /** + * @var Url + */ + private Url $url; + /** + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param Url $url + * @param array $data + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + Url $url, + array $data = [] + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->url = $url; + } + + /** + * @inheritDoc + * + * @return AbstractModel + * @throws ValidatorException + */ + public function beforeSave(): AbstractModel + { + $isValid = $this->url->isValid($this->getValue(), ['http', 'https']); + + if ($isValid) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $host = parse_url((string)$this->getValue(), \PHP_URL_HOST); + + if (!empty($host) && !preg_match('/(?:.+\.|^)fedex\.com$/i', $host)) { + throw new ValidatorException(__('Fedex API endpoint URL\'s must use fedex.com')); + } + } + + return parent::beforeSave(); + } +} diff --git a/app/code/Magento/Fedex/README.md b/app/code/Magento/Fedex/README.md index b872b53bf879e..419d9771987fb 100644 --- a/app/code/Magento/Fedex/README.md +++ b/app/code/Magento/Fedex/README.md @@ -17,12 +17,14 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_cart_index` - `checkout_index_index` ## Additional information You can get more information about delivery method in magento at the articles: + - [FedEx Configuration Settings](https://docs.magento.com/user-guide/shipping/fedex.html) - [Delivery Methods Configuration](https://docs.magento.com/user-guide/configuration/sales/delivery-methods.html) - [Add custom shipping carrier](https://developer.adobe.com/commerce/php/tutorials/frontend/custom-checkout/add-shipping-carrier/) diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php new file mode 100644 index 0000000000000..56626222312bf --- /dev/null +++ b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Fedex\Test\Unit\Model\Config\Backend; + +use Magento\Fedex\Model\Config\Backend\FedexUrl; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Validator\Url; +use Magento\Rule\Model\ResourceModel\AbstractResource; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Verify behavior of FedexUrl backend type + */ +class FedexUrlTest extends TestCase +{ + + /** + * @var FedexUrl + */ + private $urlConfig; + + /** + * @var Url + */ + private $url; + + /** + * @var Context|MockObject + */ + private $contextMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->contextMock = $this->createMock(Context::class); + $registry = $this->createMock(Registry::class); + $config = $this->createMock(ScopeConfigInterface::class); + $cacheTypeList = $this->createMock(TypeListInterface::class); + $this->url = $this->createMock(Url::class); + $resource = $this->createMock(AbstractResource::class); + $resourceCollection = $this->createMock(AbstractDb::class); + $eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $eventManagerMock->expects($this->any())->method('dispatch'); + $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($eventManagerMock); + + $this->urlConfig = $objectManager->getObject( + FedexUrl::class, + [ + 'url' => $this->url, + 'context' => $this->contextMock, + 'registry' => $registry, + 'config' => $config, + 'cacheTypeList' => $cacheTypeList, + 'resource' => $resource, + 'resourceCollection' => $resourceCollection, + ] + ); + } + + /** + * @dataProvider validDataProvider + * @param string|null $data The valid data + * @throws ValidatorException + */ + public function testBeforeSave(string $data = null): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + $this->assertTrue($this->url->isValid($data)); + } + + /** + * @dataProvider invalidDataProvider + * @param string $data The invalid data + */ + public function testBeforeSaveErrors(string $data): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->expectException('Magento\Framework\Exception\ValidatorException'); + $this->expectExceptionMessage('Fedex API endpoint URL\'s must use fedex.com'); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + } + + /** + * Validator Data Provider + * + * @return array + */ + public function validDataProvider(): array + { + return [ + [], + [null], + [''], + ['http://fedex.com'], + ['https://foo.fedex.com'], + ['http://foo.fedex.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } + + /** + * @return \string[][] + */ + public function invalidDataProvider(): array + { + return [ + ['http://fedexfoo.com'], + ['https://foofedex.com'], + ['https://fedex.com.fake.com'], + ['https://fedex.info'], + ['http://fedex.com.foo.com/foo/bar?baz=bash&fizz=buzz'], + ['http://foofedex.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } +} diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php index 124d80b7cf4e1..1d85cd923dd2e 100644 --- a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php +++ b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php @@ -55,9 +55,8 @@ protected function setUp(): void * @return void * @dataProvider toOptionArrayDataProvider */ - public function testToOptionArray($code, $methods, $result): void + public function testToOptionArray($methods, $result): void { - $this->model->code = $code; $this->shippingFedexMock->expects($this->once()) ->method('getCode') ->willReturn($methods); @@ -74,7 +73,6 @@ public function toOptionArrayDataProvider(): array { return [ [ - 'method', [ 'FEDEX_GROUND' => __('Ground'), 'FIRST_OVERNIGHT' => __('First Overnight') @@ -85,7 +83,6 @@ public function toOptionArrayDataProvider(): array ] ], [ - '', false, [] ] diff --git a/app/code/Magento/Fedex/etc/adminhtml/system.xml b/app/code/Magento/Fedex/etc/adminhtml/system.xml index f164a8e21e0ae..a200b5bda7199 100644 --- a/app/code/Magento/Fedex/etc/adminhtml/system.xml +++ b/app/code/Magento/Fedex/etc/adminhtml/system.xml @@ -40,12 +40,14 @@ </field> <field id="production_webservices_url" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Web-Services URL (Production)</label> + <backend_model>Magento\Fedex\Model\Config\Backend\FedexUrl</backend_model> <depends> <field id="sandbox_mode">0</field> </depends> </field> <field id="sandbox_webservices_url" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Web-Services URL (Sandbox)</label> + <backend_model>Magento\Fedex\Model\Config\Backend\FedexUrl</backend_model> <depends> <field id="sandbox_mode">1</field> </depends> diff --git a/app/code/Magento/Fedex/i18n/en_US.csv b/app/code/Magento/Fedex/i18n/en_US.csv index d1509d42730bc..2911ebe793f23 100644 --- a/app/code/Magento/Fedex/i18n/en_US.csv +++ b/app/code/Magento/Fedex/i18n/en_US.csv @@ -78,3 +78,4 @@ Debug,Debug "Show Method if Not Applicable","Show Method if Not Applicable" "Sort Order","Sort Order" "Can't convert a shipping cost from ""%1-%2"" for FedEx carrier.","Can't convert a shipping cost from ""%1-%2"" for FedEx carrier." +"Fedex API endpoint URL\'s must use fedex.com","Fedex API endpoint URL\'s must use fedex.com" diff --git a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php index 445ba54ac4d9c..5f9828a9c35e9 100644 --- a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php +++ b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php @@ -10,14 +10,15 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\InvalidTransitionException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Order item gift message repository object. */ -class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface +class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface, ResetAfterRequestInterface { /** - * Order factory. + * Factory for Order instances. * * @var \Magento\Sales\Model\OrderFactory */ @@ -38,7 +39,7 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor protected $storeManager; /** - * Gift message save model. + * Model for Gift message save. * * @var \Magento\GiftMessage\Model\Save */ @@ -52,8 +53,6 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor protected $helper; /** - * Message factory. - * * @var \Magento\GiftMessage\Model\MessageFactory */ protected $messageFactory; @@ -175,4 +174,12 @@ protected function getItemById($orderId, $orderItemId) } return false; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->orders = []; + } } diff --git a/app/code/Magento/GiftMessage/README.md b/app/code/Magento/GiftMessage/README.md index b63c37cc64c7d..ba3bb3962b062 100644 --- a/app/code/Magento/GiftMessage/README.md +++ b/app/code/Magento/GiftMessage/README.md @@ -36,6 +36,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Events The module dispatches the following events: + - `gift_options_prepare_items` event in the `\Magento\GiftMessage\Block\Message\Inline::getItems` method. Parameters: - `items` is a entityItems (`array` type) @@ -47,6 +48,7 @@ For information about an event in Magento 2, see [Events and observers](https:// ### Layout This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `sales_order_create_index` - `sales_order_create_load_block_data` @@ -70,11 +72,11 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\CartRepositoryInterface` - get the gift message by cart ID for specified shopping cart - set the gift message for an entire shopping cart - + - `\Magento\GiftMessage\Api\GuestCartRepositoryInterface` - get the gift message by cart ID for specified shopping cart - set the gift message for an entire shopping cart - + #### Cart Item - `\Magento\GiftMessage\Api\GuestItemRepositoryInterface` @@ -84,7 +86,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\ItemRepositoryInterface` - get the gift message for a specified item in a specified shopping cart - set the gift message for a specified item in a specified shopping cart - + #### Order - `\Magento\GiftMessage\Api\OrderItemRepositoryInterface` @@ -96,7 +98,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\OrderItemRepositoryInterface` - get the gift message for a specified item in a specified order - set the gift message for a specified item in a specified order - + For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml index 9da9c8cac1483..712f66893017a 100644 --- a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml @@ -19,6 +19,6 @@ <argument name="shippingMethod" defaultValue="" type="string"/> </arguments> - <seeInCurrentUrl url="{{CheckoutPage.url}}#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index 5eb270c12fdb1..485b403bbc34c 100644 --- a/app/code/Magento/GiftMessageGraphQl/README.md +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -16,4 +16,4 @@ Extension developers can interact with the Magento_GiftMessageGraphQl module. Fo ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GoogleAdwords/README.md b/app/code/Magento/GoogleAdwords/README.md index 2e2b275787f32..d79a7837149db 100644 --- a/app/code/Magento/GoogleAdwords/README.md +++ b/app/code/Magento/GoogleAdwords/README.md @@ -17,6 +17,7 @@ Extension developers can interact with the Magento_GoogleAdwords module. For mor ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_onepage_success` For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). diff --git a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml index 050f8711027ec..68f49ff1ebee1 100644 --- a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml +++ b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml @@ -15,6 +15,7 @@ <description value="Testing for a required Conversion ID when configuring the Google Adwords"/> <severity value="MINOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php index 0370174c0b7f4..3be62a588efa2 100644 --- a/app/code/Magento/GoogleAnalytics/Block/Ga.php +++ b/app/code/Magento/GoogleAnalytics/Block/Ga.php @@ -82,6 +82,7 @@ public function getPageName() * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#set * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#gaObjectMethods * @deprecated 100.2.0 please use getPageTrackingData method + * @see getPageTrackingData method */ public function getPageTrackingCode($accountId) { @@ -103,6 +104,7 @@ public function getPageTrackingCode($accountId) * * @return string|void * @deprecated 100.2.0 please use getOrdersTrackingData method + * @see getOrdersTrackingData method */ public function getOrdersTrackingCode() { @@ -120,17 +122,19 @@ public function getOrdersTrackingCode() foreach ($collection as $order) { $result[] = "ga('set', 'currencyCode', '" . $order->getOrderCurrencyCode() . "');"; foreach ($order->getAllVisibleItems() as $item) { + $quantity = $item->getQtyOrdered() * 1; + $format = fmod($quantity, 1) !== 0.00 ? '%.2f' : '%d'; $result[] = sprintf( "ga('ec:addProduct', { 'id': '%s', 'name': '%s', - 'price': '%s', - 'quantity': %s + 'price': %.2f, + 'quantity': $format });", $this->escapeJsQuote($item->getSku()), $this->escapeJsQuote($item->getName()), - $item->getPrice(), - $item->getQtyOrdered() + (float)$item->getPrice(), + $quantity ); } @@ -138,15 +142,15 @@ public function getOrdersTrackingCode() "ga('ec:setAction', 'purchase', { 'id': '%s', 'affiliation': '%s', - 'revenue': '%s', - 'tax': '%s', - 'shipping': '%s' + 'revenue': %.2f, + 'tax': %.2f, + 'shipping': %.2f });", $order->getIncrementId(), $this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()), - $order->getGrandTotal(), - $order->getTaxAmount(), - $order->getShippingAmount() + (float)$order->getGrandTotal(), + (float)$order->getTaxAmount(), + (float)$order->getShippingAmount(), ); $result[] = "ga('send', 'pageview');"; @@ -232,19 +236,20 @@ public function getOrdersTrackingData() foreach ($collection as $order) { foreach ($order->getAllVisibleItems() as $item) { + $quantity = $item->getQtyOrdered() * 1; $result['products'][] = [ 'id' => $this->escapeJsQuote($item->getSku()), 'name' => $this->escapeJsQuote($item->getName()), - 'price' => $item->getPrice(), - 'quantity' => $item->getQtyOrdered(), + 'price' => (float)$item->getPrice(), + 'quantity' => $quantity, ]; } $result['orders'][] = [ 'id' => $order->getIncrementId(), 'affiliation' => $this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()), - 'revenue' => $order->getGrandTotal(), - 'tax' => $order->getTaxAmount(), - 'shipping' => $order->getShippingAmount(), + 'revenue' => (float)$order->getGrandTotal(), + 'tax' => (float)$order->getTaxAmount(), + 'shipping' => (float)$order->getShippingAmount(), ]; $result['currency'] = $order->getOrderCurrencyCode(); } diff --git a/app/code/Magento/GoogleAnalytics/README.md b/app/code/Magento/GoogleAnalytics/README.md index d4abd290bd665..226871406e241 100644 --- a/app/code/Magento/GoogleAnalytics/README.md +++ b/app/code/Magento/GoogleAnalytics/README.md @@ -21,6 +21,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `default` For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). diff --git a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php index a367a938d45b9..8088b03707b2e 100644 --- a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php +++ b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php @@ -115,15 +115,21 @@ public function testOrderTrackingCode() ga('ec:addProduct', { 'id': 'sku0', 'name': 'testName0', - 'price': '0.00', + 'price': 0.00, 'quantity': 1 }); + ga('ec:addProduct', { + 'id': 'sku1', + 'name': 'testName1', + 'price': 1.00, + 'quantity': 1.11 + }); ga('ec:setAction', 'purchase', { 'id': '100', 'affiliation': 'test', - 'revenue': '10', - 'tax': '2', - 'shipping': '1' + 'revenue': 10.00, + 'tax': 2.00, + 'shipping': 2.00 }); ga('send', 'pageview');"; @@ -163,9 +169,9 @@ public function testOrderTrackingData() [ 'id' => 100, 'affiliation' => 'test', - 'revenue' => 10, - 'tax' => 2, - 'shipping' => 1 + 'revenue' => 10.00, + 'tax' => 2.00, + 'shipping' => 2.0 ] ], 'products' => [ @@ -174,6 +180,12 @@ public function testOrderTrackingData() 'name' => 'testName0', 'price' => 0.00, 'quantity' => 1 + ], + [ + 'id' => 'sku1', + 'name' => 'testName1', + 'price' => 1.00, + 'quantity' => 1.11 ] ], 'currency' => 'USD' @@ -204,7 +216,7 @@ public function testGetPageTrackingData() * @param int $orderItemCount * @return Order|MockObject */ - protected function createOrderMock($orderItemCount = 1) + protected function createOrderMock($orderItemCount = 2) { $orderItems = []; for ($i = 0; $i < $orderItemCount; $i++) { @@ -213,8 +225,8 @@ protected function createOrderMock($orderItemCount = 1) ->getMockForAbstractClass(); $orderItemMock->expects($this->once())->method('getSku')->willReturn('sku' . $i); $orderItemMock->expects($this->once())->method('getName')->willReturn('testName' . $i); - $orderItemMock->expects($this->once())->method('getPrice')->willReturn($i . '.00'); - $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i + 1); + $orderItemMock->expects($this->once())->method('getPrice')->willReturn((float)($i . '.0000')); + $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i == 1 ? 1.11 : $i + 1); $orderItems[] = $orderItemMock; } @@ -223,9 +235,9 @@ protected function createOrderMock($orderItemCount = 1) ->getMock(); $orderMock->expects($this->once())->method('getIncrementId')->willReturn(100); $orderMock->expects($this->once())->method('getAllVisibleItems')->willReturn($orderItems); - $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10); - $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2); - $orderMock->expects($this->once())->method('getShippingAmount')->willReturn($orderItemCount); + $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10.00); + $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2.00); + $orderMock->expects($this->once())->method('getShippingAmount')->willReturn(round((float)$orderItemCount, 2)); $orderMock->expects($this->once())->method('getOrderCurrencyCode')->willReturn('USD'); return $orderMock; } @@ -241,7 +253,7 @@ protected function createCollectionMock() $collectionMock->expects($this->any()) ->method('getIterator') - ->willReturn(new \ArrayIterator([$this->createOrderMock(1)])); + ->willReturn(new \ArrayIterator([$this->createOrderMock(2)])); return $collectionMock; } diff --git a/app/code/Magento/GoogleGtag/README.md b/app/code/Magento/GoogleGtag/README.md index 4d1a49ada70c7..d5985c308bbc2 100644 --- a/app/code/Magento/GoogleGtag/README.md +++ b/app/code/Magento/GoogleGtag/README.md @@ -21,6 +21,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `default` - `checkout_onepage_success` diff --git a/app/code/Magento/GoogleOptimizer/README.md b/app/code/Magento/GoogleOptimizer/README.md index 5e493d69cf13b..2d2a32562f828 100644 --- a/app/code/Magento/GoogleOptimizer/README.md +++ b/app/code/Magento/GoogleOptimizer/README.md @@ -22,6 +22,7 @@ Extension developers can interact with the Magento_GoogleOptimizer module. For m ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `catalog_product_new` - `cms_page_edit` @@ -35,13 +36,14 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `category_form` - `cms_page_form` - `new_category_form` For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). -## Additional information +## Additional information Google Experiment (on Google side) allows to make two variants of the same page and compare their popularity. From Magento side, code generated by Google should be saved and displayed on a particular page. diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index f03079c89bc68..f20956407c258 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -20,6 +20,7 @@ use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; use Magento\Framework\GraphQl\Query\Fields as QueryFields; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\GraphQl\Query\QueryProcessor; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface; @@ -41,6 +42,7 @@ class GraphQl implements FrontControllerInterface /** * @var \Magento\Framework\Webapi\Response * @deprecated 100.3.2 + * @see no replacement */ private $response; @@ -66,7 +68,8 @@ class GraphQl implements FrontControllerInterface /** * @var ContextInterface - * @deprecated 100.3.3 $contextFactory is used for creating Context object + * @deprecated 100.3.3 + * @see $contextFactory is used for creating Context object */ private $resolverContext; @@ -110,6 +113,11 @@ class GraphQl implements FrontControllerInterface */ private $areaList; + /** + * @var QueryParser + */ + private $queryParser; + /** * @param Response $response * @param SchemaGeneratorInterface $schemaGenerator @@ -125,6 +133,7 @@ class GraphQl implements FrontControllerInterface * @param LogData|null $logDataHelper * @param LoggerPool|null $loggerPool * @param AreaList|null $areaList + * @param QueryParser|null $queryParser * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -141,7 +150,8 @@ public function __construct( ContextFactoryInterface $contextFactory = null, LogData $logDataHelper = null, LoggerPool $loggerPool = null, - AreaList $areaList = null + AreaList $areaList = null, + QueryParser $queryParser = null ) { $this->response = $response; $this->schemaGenerator = $schemaGenerator; @@ -157,6 +167,7 @@ public function __construct( $this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class); $this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class); $this->areaList = $areaList ?: ObjectManager::getInstance()->get(AreaList::class); + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } /** @@ -179,18 +190,18 @@ public function dispatch(RequestInterface $request): ResponseInterface try { /** @var Http $request */ $this->requestProcessor->validateRequest($request); - $query = $data['query'] ?? ''; - $variables = $data['variables'] ?? null; + $parsedQuery = $this->queryParser->parse($query); + $data['parsedQuery'] = $parsedQuery; // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema // Temporal coupling is required for performance optimization - $this->queryFields->setQuery($query, $variables); + $this->queryFields->setQuery($parsedQuery, $data['variables'] ?? null); $schema = $this->schemaGenerator->generate(); $result = $this->queryProcessor->process( $schema, - $query, + $parsedQuery, $this->contextFactory->create(), $data['variables'] ?? [] ); diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php index d42676f5dd1b4..56351c7711cec 100644 --- a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -9,12 +9,12 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\Http; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\Phrase; use Magento\GraphQl\Controller\HttpRequestValidatorInterface; @@ -23,6 +23,19 @@ */ class HttpVerbValidator implements HttpRequestValidatorInterface { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Check if request is using correct verb for query or mutation * @@ -37,9 +50,9 @@ public function validate(HttpRequestInterface $request): void $query = $request->getParam('query', ''); if (!empty($query)) { $operationType = ''; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + $parsedQuery = $this->queryParser->parse($query); Visitor::visit( - $queryAst, + $parsedQuery, [ 'leave' => [ NodeKind::OPERATION_DEFINITION => function (Node $node) use (&$operationType) { diff --git a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php index 91e2518bfc634..fd45ef93cf13b 100644 --- a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php +++ b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php @@ -8,13 +8,14 @@ namespace Magento\GraphQl\Helper\Query\Logger; use GraphQL\Error\SyntaxError; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\GraphQl\Schema; use Magento\GraphQl\Model\Query\Logger\LoggerInterface; @@ -23,6 +24,19 @@ */ class LogData { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Extracts relevant information about the request * @@ -43,7 +57,7 @@ public function getLogData( $logData = array_merge($logData, $this->gatherRequestInformation($request)); try { - $complexity = $this->getFieldCount($data['query'] ?? ''); + $complexity = $this->getFieldCount($data['parsedQuery'] ?? $data['query'] ?? ''); $logData[LoggerInterface::COMPLEXITY] = $complexity; if ($schema) { $logData = array_merge($logData, $this->gatherQueryInformation($schema)); @@ -114,18 +128,20 @@ private function gatherResponseInformation(HttpResponse $response) : array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @param string $query + * @param DocumentNode|string $query * @return int * @throws SyntaxError - * @throws /Exception + * @throws \Exception */ - private function getFieldCount(string $query): int + private function getFieldCount(DocumentNode|string $query): int { if (!empty($query)) { $totalFieldCount = 0; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) { diff --git a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php index 5edbf4e207c91..c9f1c943a71e3 100644 --- a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php +++ b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php @@ -43,6 +43,7 @@ public function __construct( /** * Validate resolver args * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param Field $field * @param array $args * @return void diff --git a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php index d8fa03657e401..5eb03d4ed13d2 100644 --- a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php +++ b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php @@ -9,13 +9,15 @@ use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; /** * @inheritdoc */ -class ContextFactory implements ContextFactoryInterface +class ContextFactory implements ContextFactoryInterface, ResetAfterRequestInterface { /** * @var ExtensionAttributesFactory @@ -58,15 +60,16 @@ public function __construct( public function create(?UserContextInterface $userContext = null): ContextInterface { $contextParameters = $this->objectManager->create(ContextParametersInterface::class); - foreach ($this->contextParametersProcessors as $contextParametersProcessor) { if (!$contextParametersProcessor instanceof ContextParametersProcessorInterface) { throw new LocalizedException( __('ContextParametersProcessors must implement %1', ContextParametersProcessorInterface::class) ); } - if ($userContext && $contextParametersProcessor instanceof UserContextParametersProcessorInterface) { - $contextParametersProcessor->setUserContext($userContext); + if ($contextParametersProcessor instanceof UserContextParametersProcessorInterface) { + $contextParametersProcessor->setUserContext( + $userContext ?? $this->objectManager->create(UserContextInterface::class) + ); } $contextParameters = $contextParametersProcessor->execute($contextParameters); } @@ -100,4 +103,12 @@ public function get(): ContextInterface } return $this->context; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->context = null; + } } diff --git a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php index 55f25c176ed43..248797896389a 100644 --- a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php +++ b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php @@ -41,6 +41,9 @@ public function __construct( */ public function execute(array $queryDetails) { + $transactionName = $queryDetails[LoggerInterface::OPERATION_NAMES] ?? ''; + $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName); + if (!$this->config->isNewRelicEnabled()) { return; } @@ -48,9 +51,5 @@ public function execute(array $queryDetails) foreach ($queryDetails as $key => $value) { $this->newRelicWrapper->addCustomParameter($key, $value); } - - $transactionName = $queryDetails[LoggerInterface::OPERATION_NAMES] ?: ''; - - $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName); } } diff --git a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php index 9403ccaf07099..bae3ceabf2783 100644 --- a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php +++ b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php @@ -8,19 +8,21 @@ namespace Magento\GraphQl\Model\Query\Resolver; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Do not use this class. It was kept for backward compatibility. * - * @deprecated 100.3.3 \Magento\GraphQl\Model\Query\Context is used instead of this + * @deprecated 100.3.3 + * @see \Magento\GraphQl\Model\Query\Context */ class Context extends \Magento\Framework\Model\AbstractExtensibleModel implements ContextInterface { /**#@+ * Constants defined for type of context */ - const USER_TYPE_ID = 'user_type'; - const USER_ID = 'user_id'; + public const USER_TYPE_ID = 'user_type'; + public const USER_ID = 'user_id'; /**#@-*/ /** @@ -86,4 +88,12 @@ public function setUserType(int $typeId) : ContextInterface { return $this->setData(self::USER_TYPE_ID, $typeId); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_data = []; + } } diff --git a/app/code/Magento/GraphQl/README.md b/app/code/Magento/GraphQl/README.md index de575fae59b5f..1372e5760e936 100644 --- a/app/code/Magento/GraphQl/README.md +++ b/app/code/Magento/GraphQl/README.md @@ -1,7 +1,7 @@ # Magento_GraphQl module This module provides the framework for the application to expose GraphQL compliant web services. It exposes an area for -GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object +GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object for the client to read. ## Installation @@ -9,10 +9,12 @@ for the client to read. The Magento_GraphQl module is one of the base Magento 2 modules. You cannot disable or uninstall this module. This module is dependent on the following modules: + - `Magento_Authorization` - `Magento_Eav` The following modules depend on this module: + - `Magento_BundleGraphQl` - `Magento_CatalogGraphQl` - `Magento_CmsGraphQl` @@ -35,4 +37,4 @@ Extension developers can interact with the Magento_GraphQl module. For more info ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index b81c3a924d4e5..af1fe042c6df5 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -9,7 +9,7 @@ "magento/module-webapi": "*", "magento/module-new-relic-reporting": "*", "magento/module-authorization": "*", - "webonyx/graphql-php": "^14.11" + "webonyx/graphql-php": "^15.0" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 1ba190cd8bb22..67fb0d4e1b268 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -76,7 +76,7 @@ input FilterRangeTypeInput @doc(description: "Defines a filter that matches a ra } input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") { - match: String @doc(description: "Use this attribute to exactly match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.") + match: String @doc(description: "Use this attribute to fuzzy match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.") } input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") { diff --git a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php index a594dcd6148f5..c4ce6cb4e7ec7 100644 --- a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php +++ b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php @@ -8,6 +8,7 @@ namespace Magento\GraphQlCache\Controller\Plugin; use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http as ResponseHttp; use Magento\Framework\Controller\ResultInterface; @@ -16,9 +17,11 @@ use Magento\GraphQlCache\Model\CacheableQuery; use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\PageCache\Model\Config; +use Psr\Log\LoggerInterface; /** * Plugin for handling controller after controller tags and pre-controller validation. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GraphQl { @@ -32,11 +35,6 @@ class GraphQl */ private $config; - /** - * @var ResponseHttp - */ - private $response; - /** * @var HttpRequestProcessor */ @@ -52,28 +50,37 @@ class GraphQl */ private $cacheIdCalculator; + /** + * @var LoggerInterface $logger + */ + private $logger; + /** * @param CacheableQuery $cacheableQuery + * @param CacheIdCalculator $cacheIdCalculator * @param Config $config - * @param ResponseHttp $response + * @param LoggerInterface $logger * @param HttpRequestProcessor $requestProcessor + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param ResponseHttp $response @deprecated do not use * @param Registry $registry - * @param CacheIdCalculator $cacheIdCalculator */ public function __construct( CacheableQuery $cacheableQuery, + CacheIdCalculator $cacheIdCalculator, Config $config, - ResponseHttp $response, + LoggerInterface $logger, HttpRequestProcessor $requestProcessor, - Registry $registry, - CacheIdCalculator $cacheIdCalculator + ResponseHttp $response, + Registry $registry = null ) { $this->cacheableQuery = $cacheableQuery; + $this->cacheIdCalculator = $cacheIdCalculator; $this->config = $config; - $this->response = $response; + $this->logger = $logger; $this->requestProcessor = $requestProcessor; - $this->registry = $registry; - $this->cacheIdCalculator = $cacheIdCalculator; + $this->registry = $registry ?: ObjectManager::getInstance() + ->get(Registry::class); } /** @@ -87,7 +94,12 @@ public function __construct( public function beforeDispatch( FrontControllerInterface $subject, RequestInterface $request - ) { + ): void { + try { + $this->requestProcessor->validateRequest($request); + } catch (\Exception $error) { + $this->logger->critical($error->getMessage()); + } /** @var \Magento\Framework\App\Request\Http $request */ $this->requestProcessor->processHeaders($request); } @@ -109,26 +121,22 @@ public function afterRenderResult(ResultInterface $subject, ResultInterface $res /** @see \Magento\Framework\App\Http::launch */ /** @see \Magento\PageCache\Model\Controller\Result\BuiltinPlugin::afterRenderResult */ $this->registry->register('use_page_cache_plugin', true, true); - $cacheId = $this->cacheIdCalculator->getCacheId(); if ($cacheId) { - $this->response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true); + $response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true); } - if ($this->cacheableQuery->shouldPopulateCacheHeadersWithTags()) { - $this->response->setPublicHeaders($this->config->getTtl()); - $this->response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); + $response->setPublicHeaders($this->config->getTtl()); + $response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); } else { $sendNoCacheHeaders = true; } } else { $sendNoCacheHeaders = true; } - if ($sendNoCacheHeaders) { - $this->response->setNoCacheHeaders(); + $response->setNoCacheHeaders(); } - return $result; } } diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php index 451e1039eec57..da29ccb83bb75 100644 --- a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php +++ b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php @@ -7,10 +7,12 @@ namespace Magento\GraphQlCache\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** - * CacheableQuery should be used as a singleton for collecting cache related info and tags of all entities. + * CacheableQuery should be used as a singleton for collecting HTTP cache-related info and tags of all entities. */ -class CacheableQuery +class CacheableQuery implements ResetAfterRequestInterface { /** * @var string[] @@ -40,11 +42,11 @@ public function getCacheTags(): array */ public function addCacheTags(array $cacheTags): void { - $this->cacheTags = array_merge($this->cacheTags, $cacheTags); + $this->cacheTags = array_unique(array_merge($this->cacheTags, $cacheTags)); } /** - * Return if its valid to cache the response + * Return if it's valid to cache the response * * @return bool */ @@ -54,7 +56,7 @@ public function isCacheable(): bool } /** - * Set cache validity + * Set HTTP full page cache validity * * @param bool $cacheable */ @@ -71,7 +73,17 @@ public function setCacheValidity(bool $cacheable): void public function shouldPopulateCacheHeadersWithTags() : bool { $cacheTags = $this->getCacheTags(); - $isQueryCaheable = $this->isCacheable(); - return !empty($cacheTags) && $isQueryCaheable; + $isQueryCacheable = $this->isCacheable(); + + return !empty($cacheTags) && $isQueryCacheable; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheTags = []; + $this->cacheable = true; } } diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php index 53f5155f8a3ac..d43fd26dc5f54 100644 --- a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php +++ b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php @@ -7,13 +7,12 @@ namespace Magento\GraphQlCache\Model; -use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Request\Http; use Magento\GraphQlCache\Model\Resolver\IdentityPool; /** - * Handler of collecting tagging on cache. + * Handler for collecting tags on HTTP full page cache. * * This class would be used to collect tags after each operation where we need to collect tags * usually after data is fetched or resolved. @@ -51,7 +50,7 @@ public function __construct( } /** - * Set cache validity to the cacheableQuery after resolving any resolver or evaluating a promise in a query + * Set HTTP full page cache validity on $cacheableQuery after resolving any resolver in a query * * @param array $resolvedValue * @param array $cacheAnnotation Eg: ['cacheable' => true, 'cacheTag' => 'someTag', cacheIdentity=>'\Mage\Class'] @@ -69,11 +68,12 @@ public function handleCacheFromResolverResponse(array $resolvedValue, array $cac } else { $cacheable = false; } + $this->setCacheValidity($cacheable); } /** - * Set cache validity for the graphql request + * Set HTTP full page cache validity for the graphql request * * @param bool $isValid * @return void diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php index 10fe5739c461e..e5a703de3399e 100644 --- a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php +++ b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php @@ -13,7 +13,7 @@ use Magento\GraphQlCache\Model\CacheableQueryHandler; /** - * Plugin to handle cache validation that can be done after each resolver + * Plugin to handle HTTP cache validation that can be done after each resolver */ class Resolver { diff --git a/app/code/Magento/GraphQlCache/README.md b/app/code/Magento/GraphQlCache/README.md index 32555f1423666..85e03391eb9ee 100644 --- a/app/code/Magento/GraphQlCache/README.md +++ b/app/code/Magento/GraphQlCache/README.md @@ -1,7 +1,7 @@ # Magento_GraphQlCache module This module provides the ability to cache GraphQL queries. -This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end. +This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end. ## Installation @@ -20,5 +20,5 @@ Extension developers can interact with the Magento_GraphQlCache module. For more ## Additional information -- [Learn more about GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). - [Learn more about GraphQl Caching In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/usage/caching/). diff --git a/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php new file mode 100644 index 0000000000000..dd45b3c715f9e --- /dev/null +++ b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Test\Unit\Controller\Plugin; + +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\Http as ResponseHttp; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GraphQl\Controller\HttpRequestProcessor; +use Magento\GraphQlCache\Controller\Plugin\GraphQl; +use Magento\GraphQlCache\Model\CacheableQuery; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\PageCache\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test beforeDispatch + */ +class GraphQlTest extends TestCase +{ + /** + * @var GraphQl + */ + private $graphql; + + /** + * @var CacheableQuery|MockObject + */ + private $cacheableQueryMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var ResponseHttp|MockObject + */ + private $responseMock; + + /** + * @var HttpRequestProcessor|MockObject + */ + private $requestProcessorMock; + + /** + * @var CacheIdCalculator|MockObject + */ + private $cacheIdCalculatorMock; + + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + + /** + * @var FrontControllerInterface|MockObject + */ + private $subjectMock; + + /** + * @var Http|MockObject + */ + private $requestMock; + + protected function setUp(): void + { + $this->cacheableQueryMock = $this->createMock(CacheableQuery::class); + $this->cacheIdCalculatorMock = $this->createMock(CacheIdCalculator::class); + $this->configMock = $this->createMock(Config::class); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->onlyMethods(['critical']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->requestProcessorMock = $this->getMockBuilder(HttpRequestProcessor::class) + ->onlyMethods(['validateRequest','processHeaders']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->responseMock = $this->createMock(ResponseHttp::class); + $this->subjectMock = $this->createMock(FrontControllerInterface::class); + $this->requestMock = $this->createMock(Http::class); + $this->graphql = new GraphQl( + $this->cacheableQueryMock, + $this->cacheIdCalculatorMock, + $this->configMock, + $this->loggerMock, + $this->requestProcessorMock, + $this->responseMock + ); + } + + /** + * test beforeDispatch function for validation purpose + */ + public function testBeforeDispatch(): void + { + $this->requestProcessorMock + ->expects($this->any()) + ->method('validateRequest'); + $this->requestProcessorMock + ->expects($this->any()) + ->method('processHeaders'); + $this->loggerMock + ->expects($this->any()) + ->method('critical'); + $this->assertNull($this->graphql->beforeDispatch($this->subjectMock, $this->requestMock)); + } +} diff --git a/app/code/Magento/GraphQlCache/etc/graphql/di.xml b/app/code/Magento/GraphQlCache/etc/graphql/di.xml index 1270ba24c94bb..1a85f02b5be9c 100644 --- a/app/code/Magento/GraphQlCache/etc/graphql/di.xml +++ b/app/code/Magento/GraphQlCache/etc/graphql/di.xml @@ -12,7 +12,7 @@ <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"/> </type> <type name="Magento\Framework\GraphQl\Query\ResolverInterface"> - <plugin name="cache" type="Magento\GraphQlCache\Model\Plugin\Query\Resolver"/> + <plugin name="cache" type="Magento\GraphQlCache\Model\Plugin\Query\Resolver" sortOrder="10"/> </type> <type name="Magento\Framework\App\PageCache\Identifier"> <plugin name="core-app-area-design-exception-plugin" diff --git a/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php b/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php new file mode 100644 index 0000000000000..ccf3bb9727201 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Plugin\Resolver; + +use Magento\Framework\App\Cache\StateInterface as CacheState; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\CalculationException; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Plugin to cache resolver result where applicable. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Cache +{ + /** + * GraphQL Resolver cache type + * + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var CacheState + */ + private $cacheState; + + /** + * @var ResolverIdentityClassProvider + */ + private $resolverIdentityClassProvider; + + /** + * @var ValueProcessorInterface + */ + private ValueProcessorInterface $valueProcessor; + + /** + * @var ProviderInterface + */ + private ProviderInterface $keyCalculatorProvider; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param GraphQlResolverCache $graphQlResolverCache + * @param SerializerInterface $serializer + * @param CacheState $cacheState + * @param ResolverIdentityClassProvider $resolverIdentityClassProvider + * @param ValueProcessorInterface $valueProcessor + * @param ProviderInterface $keyCalculatorProvider + * @param LoggerInterface $logger + */ + public function __construct( + GraphQlResolverCache $graphQlResolverCache, + SerializerInterface $serializer, + CacheState $cacheState, + ResolverIdentityClassProvider $resolverIdentityClassProvider, + ValueProcessorInterface $valueProcessor, + ProviderInterface $keyCalculatorProvider, + LoggerInterface $logger + ) { + $this->graphQlResolverCache = $graphQlResolverCache; + $this->serializer = $serializer; + $this->cacheState = $cacheState; + $this->resolverIdentityClassProvider = $resolverIdentityClassProvider; + $this->valueProcessor = $valueProcessor; + $this->keyCalculatorProvider = $keyCalculatorProvider; + $this->logger = $logger; + } + + /** + * Checks for cacheability of resolver's data, and, if cacheable, loads and persists cache entry for future use + * + * @param ResolverInterface $subject + * @param \Closure $proceed + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return mixed|Value + */ + public function aroundResolve( + ResolverInterface $subject, + \Closure $proceed, + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + // even though a frontend access proxy is used to prevent saving/loading in $graphQlResolverCache when it is + // disabled, it's best to return as early as possible to avoid unnecessary processing + if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER) + || $info->operation->operation !== 'query' + ) { + return $proceed($field, $context, $info, $value, $args); + } + + $identityProvider = $this->resolverIdentityClassProvider->getIdentityFromResolver($subject); + + if (!$identityProvider) { // not cacheable; proceed + return $this->executeResolver($proceed, $field, $context, $info, $value, $args); + } + + // Cache key provider may base cache key on the parent resolver value + // $value is processed on key calculation if needed + try { + $cacheKey = $this->prepareCacheIdentifier($subject, $args, $value); + } catch (CalculationException $e) { + $this->logger->warning( + sprintf( + "Unable to obtain cache key for %s resolver results, proceeding to invoke resolver." + . "Original exception message: %s ", + get_class($subject), + $e->getMessage() + ) + ); + return $this->executeResolver($proceed, $field, $context, $info, $value, $args); + } + + $cachedResult = $this->graphQlResolverCache->load($cacheKey); + + if ($cachedResult !== false) { + $returnValue = $this->serializer->unserialize($cachedResult); + $this->valueProcessor->processCachedValueAfterLoad($info, $subject, $cacheKey, $returnValue); + return $returnValue; + } + + $returnValue = $this->executeResolver($proceed, $field, $context, $info, $value, $args); + + // $value (parent value) is preprocessed (hydrated) on the previous step + $identities = $identityProvider->getIdentities($returnValue, $value); + + if (count($identities)) { + $cachedValue = $returnValue; + $this->valueProcessor->preProcessValueBeforeCacheSave($subject, $cachedValue); + $this->graphQlResolverCache->save( + $this->serializer->serialize($cachedValue), + $cacheKey, + $identities, + false // use default lifetime directive + ); + unset($cachedValue); + } + + return $returnValue; + } + + /** + * Call proceed method with context. + * + * @param \Closure $closure + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return mixed + */ + private function executeResolver( + \Closure $closure, + Field $field, + ContextInterface $context, + ResolveInfo $info, + array &$value = null, + array $args = null + ) { + if (is_array($value)) { + $this->valueProcessor->preProcessParentValue($value); + } + return $closure($field, $context, $info, $value, $args); + } + + /** + * Generate cache key incorporating factors from parameters. + * + * @param ResolverInterface $resolver + * @param array|null $args + * @param array|null $value + * + * @return string + * @throws CalculationException + */ + private function prepareCacheIdentifier( + ResolverInterface $resolver, + ?array $args, + ?array $value + ): string { + $queryPayloadHash = sha1(get_class($resolver) . $this->serializer->serialize($args ?? [])); + + return GraphQlResolverCache::CACHE_TAG + . '_' + . $this->keyCalculatorProvider->getKeyCalculatorForResolver($resolver)->calculateCacheKey($value) + . '_' + . $queryPayloadHash; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php new file mode 100644 index 0000000000000..659967a2a7ff5 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\Cache; + +/** + * Resolver cache identity interface. + */ +interface IdentityInterface +{ + + /** + * Get identity tags from resolved and parent resolver result data. + * + * Example: identityTag, identityTag_UniqueId. + * + * @param mixed $resolvedData + * @param array|null $parentResolvedData + * @return string[] + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php new file mode 100644 index 0000000000000..957cbea16a29d --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +class CalculationException extends \Exception +{ + +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php new file mode 100644 index 0000000000000..e5f264f65a085 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\Model\Query\ContextFactoryInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Calculates cache key for the resolver results. + */ +class Calculator +{ + /** + * @var ContextFactoryInterface + */ + private $contextFactory; + + /** + * @var string[] + */ + private $factorProviders; + + /** + * @var GenericFactorProviderInterface[] + */ + private $factorProviderInstances; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @var ValueProcessorInterface + */ + private ValueProcessorInterface $valueProcessor; + + /** + * @param ContextFactoryInterface $contextFactory + * @param ObjectManagerInterface $objectManager + * @param ValueProcessorInterface $valueProcessor + * @param string[] $factorProviders + */ + public function __construct( + ContextFactoryInterface $contextFactory, + ObjectManagerInterface $objectManager, + ValueProcessorInterface $valueProcessor, + array $factorProviders = [] + ) { + $this->contextFactory = $contextFactory; + $this->factorProviders = $factorProviders; + $this->objectManager = $objectManager; + $this->valueProcessor = $valueProcessor; + } + + /** + * Calculates the value of resolver cache identifier. + * + * @param array|null $parentData + * + * @return string|null + * + * @throws CalculationException + */ + public function calculateCacheKey(?array $parentData = null): ?string + { + if (!$this->factorProviders) { + return null; + } + try { + $this->initializeFactorProviderInstances(); + $factors = $this->getFactors($parentData); + $keysString = strtoupper(implode('|', array_values($factors))); + return hash('sha256', $keysString); + } catch (\Throwable $e) { + throw new CalculationException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Get key factors from parent data for current context. + * + * @param array|null $parentData + * @return array + */ + private function getFactors(?array $parentData): array + { + $factors = []; + $context = $this->contextFactory->get(); + foreach ($this->factorProviderInstances as $factorProvider) { + if ($factorProvider instanceof ParentValueFactorProviderInterface && is_array($parentData)) { + // preprocess data if the data was fetched from cache and has reference key + // and the factorProvider expects processed data (original data from resolver) + if (isset($parentData[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]) + && $factorProvider->isRequiredOrigData() + ) { + $this->valueProcessor->preProcessParentValue($parentData); + } + // fetch factor value considering parent data + $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue( + $context, + $parentData + ); + } else { + // get factor value considering only context + $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue( + $context + ); + } + } + ksort($factors); + return $factors; + } + + /** + * Initialize instances of factor providers. + * + * @return void + */ + private function initializeFactorProviderInstances(): void + { + if (empty($this->factorProviderInstances) && !empty($this->factorProviders)) { + foreach ($this->factorProviders as $factorProviderClass) { + $this->factorProviderInstances[] = $this->objectManager->get($factorProviderClass); + } + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php new file mode 100644 index 0000000000000..994b9570148e7 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +/** + * Provides custom cache key calculators for the resolvers chain. + */ +class Provider implements ProviderInterface +{ + /** + * @var array + */ + private array $customFactorProviders = []; + + /** + * @var array + */ + private array $keyCalculatorInstances = []; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $customFactorProviders + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $customFactorProviders = [] + ) { + $this->objectManager = $objectManager; + $this->customFactorProviders = $customFactorProviders; + } + + /** + * Initialize custom cache key calculator for the given resolver. + * + * @param ResolverInterface $resolver + * + * @return void + */ + private function initForResolver(ResolverInterface $resolver): void + { + $resolverClass = trim(get_class($resolver), '\\'); + if (isset($this->keyCalculatorInstances[$resolverClass])) { + return; + } + $customKeyFactorProviders = $this->getCustomFactorProvidersForResolver($resolver); + if (empty($customKeyFactorProviders)) { + $this->keyCalculatorInstances[$resolverClass] = $this->objectManager->get(Calculator::class); + } else { + $runtimePoolKey = $this->generateCustomProvidersKey($customKeyFactorProviders); + if (!isset($this->keyCalculatorInstances[$runtimePoolKey])) { + $this->keyCalculatorInstances[$runtimePoolKey] = $this->objectManager->create( + Calculator::class, + ['factorProviders' => $customKeyFactorProviders] + ); + } + $this->keyCalculatorInstances[$resolverClass] = $this->keyCalculatorInstances[$runtimePoolKey]; + } + } + + /** + * Generate runtime pool key from the set of custom providers. + * + * @param array $customProviders + * @return string + */ + private function generateCustomProvidersKey(array $customProviders): string + { + $keyArray = array_keys($customProviders); + sort($keyArray); + return implode('_', $keyArray); + } + + /** + * @inheritDoc + */ + public function getKeyCalculatorForResolver(ResolverInterface $resolver): Calculator + { + $resolverClass = trim(get_class($resolver), '\\'); + if (!isset($this->keyCalculatorInstances[$resolverClass])) { + $this->initForResolver($resolver); + } + return $this->keyCalculatorInstances[$resolverClass]; + } + + /** + * Get class inheritance chain for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array + */ + private function getResolverClassChain(ResolverInterface $resolver): array + { + $resolverClasses = [trim(get_class($resolver), '\\')]; + foreach (class_parents($resolver) as $classParent) { + $resolverClasses[] = trim($classParent, '\\'); + } + return $resolverClasses; + } + + /** + * Get custom cache key factor providers for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array + */ + private function getCustomFactorProvidersForResolver(ResolverInterface $resolver): array + { + foreach ($this->getResolverClassChain($resolver) as $resolverClass) { + if (!empty($this->customFactorProviders[$resolverClass])) { + return $this->customFactorProviders[$resolverClass]; + } + } + return []; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php new file mode 100644 index 0000000000000..d7efbf45d32cf --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +/** + * Interface for custom cache key calculator provider. + */ +interface ProviderInterface +{ + /** + * Get cache key calculator for the given resolver. + * + * @param ResolverInterface $resolver + * @return Calculator + */ + public function getKeyCalculatorForResolver(ResolverInterface $resolver): Calculator; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php new file mode 100644 index 0000000000000..7bbb64a2f6e0c --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Interface for key factors that are used to calculate the resolver cache key. + */ +interface GenericFactorProviderInterface +{ + /** + * Name of the cache key factor. + * + * @return string + */ + public function getFactorName(): string; + + /** + * Returns the runtime value that should be used as factor. + * + * Throws an Exception if factor value cannot be resolved. + * + * @param ContextInterface $context + * + * @throws \Exception + * + * @return string + */ + public function getFactorValue(ContextInterface $context): string; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php new file mode 100644 index 0000000000000..1227a939e181b --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Interface for key factors that are used to calculate the resolver cache key basing on parent value. + */ +interface ParentValueFactorProviderInterface +{ + /** + * Name of the cache key factor. + * + * @return string + */ + public function getFactorName(): string; + + /** + * Checks if the original resolver data required. + * + * Must return true if any: + * - original resolved data is required to resolve key factor + * + * Can return false if any: + * - key factor can be resolved from unprocessed cached value + * + * @return bool + */ + public function isRequiredOrigData(): bool; + + /** + * Returns the runtime value that should be used as factor. + * + * Throws an Exception if factor value cannot be resolved. + * + * @param ContextInterface $context + * @param array $parentValue + * + * @throws \Exception + * + * @return string + */ + public function getFactorValue(ContextInterface $context, array $parentValue): string; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php new file mode 100644 index 0000000000000..808a2705d881a --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Composite dehydrator for resolver result data. + */ +class DehydratorComposite implements DehydratorInterface +{ + /** + * @var DehydratorInterface[] + */ + private array $dehydrators = []; + + /** + * @param DehydratorInterface[] $dehydrators + */ + public function __construct(array $dehydrators = []) + { + $this->dehydrators = $dehydrators; + } + + /** + * @inheritdoc + */ + public function dehydrate(array &$resolvedValue): void + { + if (empty($resolvedValue)) { + return; + } + foreach ($this->dehydrators as $dehydrator) { + $dehydrator->dehydrate($resolvedValue); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php new file mode 100644 index 0000000000000..2616bdb0e2137 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Dehydrates resolved value into serializable restorable snapshots. + */ +interface DehydratorInterface +{ + /** + * Dehydrate value into restorable snapshots. + * + * @param array $resolvedValue + * @return void + */ + public function dehydrate(array &$resolvedValue): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php new file mode 100644 index 0000000000000..ce03cb0ff4699 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * Interface for resolver-based dehydrator provider. + */ +interface DehydratorProviderInterface +{ + /** + * Returns dehydrator for the given resolver, null if no dehydrators configured. + * + * @param ResolverInterface $resolver + * + * @return DehydratorInterface|null + */ + public function getDehydratorForResolver(ResolverInterface $resolver) : ?DehydratorInterface; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php new file mode 100644 index 0000000000000..d08323f2efddf --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Composite hydrator for resolver result data. + */ +class HydratorComposite implements HydratorInterface +{ + /** + * @var HydratorInterface[] + */ + private array $hydrators = []; + + /** + * @param HydratorInterface[] $hydrators + */ + public function __construct(array $hydrators = []) + { + $this->hydrators = $hydrators; + } + + /** + * @inheritdoc + */ + public function hydrate(array &$resolverData): void + { + if (empty($resolverData)) { + return; + } + foreach ($this->hydrators as $hydrator) { + $hydrator->hydrate($resolverData); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php new file mode 100644 index 0000000000000..a8e4cee7f8705 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php @@ -0,0 +1,215 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Provides hydrators and dehydrators for the given resolver. + */ +class HydratorDehydratorProvider implements HydratorProviderInterface, DehydratorProviderInterface +{ + /** + * @var array + */ + private array $dehydratorConfig = []; + + /** + * @var DehydratorInterface[] + */ + private array $dehydratorInstances = []; + + /** + * @var array + */ + private array $hydratorConfig = []; + + /** + * @var HydratorInterface[] + */ + private array $hydratorInstances = []; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $hydratorConfig + * @param array $dehydratorConfig + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $hydratorConfig = [], + array $dehydratorConfig = [] + ) { + $this->objectManager = $objectManager; + $this->dehydratorConfig = $dehydratorConfig; + $this->hydratorConfig = $hydratorConfig; + } + + /** + * @inheritdoc + */ + public function getDehydratorForResolver(ResolverInterface $resolver): ?DehydratorInterface + { + $resolverClass = $this->getResolverClass($resolver); + if (array_key_exists($resolverClass, $this->dehydratorInstances)) { + return $this->dehydratorInstances[$resolverClass]; + } + $resolverDehydrators = $this->getInstancesForResolver( + $resolver, + $this->dehydratorConfig, + DehydratorInterface::class + ); + if (empty($resolverDehydrators)) { + $this->dehydratorInstances[$resolverClass] = null; + } else { + $this->dehydratorInstances[$resolverClass] = $this->objectManager->create( + DehydratorComposite::class, + [ + 'dehydrators' => $resolverDehydrators + ] + ); + } + return $this->dehydratorInstances[$resolverClass]; + } + + /** + * @inheritDoc + */ + public function getHydratorForResolver(ResolverInterface $resolver): ?HydratorInterface + { + $resolverClass = $this->getResolverClass($resolver); + if (array_key_exists($resolverClass, $this->hydratorInstances)) { + return $this->hydratorInstances[$resolverClass]; + } + $resolverHydrators = $this->getInstancesForResolver( + $resolver, + $this->hydratorConfig, + HydratorInterface::class + ); + if (empty($resolverHydrators)) { + $this->hydratorInstances[$resolverClass] = null; + } else { + $this->hydratorInstances[$resolverClass] = $this->objectManager->create( + HydratorComposite::class, + [ + 'hydrators' => $resolverHydrators + ] + ); + } + return $this->hydratorInstances[$resolverClass]; + } + + /** + * Get resolver instance class name. + * + * @param ResolverInterface $resolver + * @return string + */ + private function getResolverClass(ResolverInterface $resolver): string + { + return trim(get_class($resolver), '\\'); + } + + /** + * Get hydrator or dehydrator instances for the given resolver from given configuration. + * + * @param ResolverInterface $resolver + * @param array $classesConfig + * @param string $interfaceName + * @return array + * @throws ConfigurationMismatchException + */ + private function getInstancesForResolver( + ResolverInterface $resolver, + array $classesConfig, + string $interfaceName + ): array { + $resolverClassesConfig = []; + foreach ($this->getResolverClassChain($resolver) as $resolverClass) { + if (isset($classesConfig[$resolverClass])) { + $resolverClassesConfig[$resolverClass] = $classesConfig[$resolverClass]; + } + } + if (empty($resolverClassesConfig)) { + return []; + } + $dataProcessingClassList = []; + foreach ($resolverClassesConfig as $resolverClass => $classChain) { + $this->validateClassChain($classChain, $interfaceName, $resolverClass); + foreach ($classChain as $classData) { + $dataProcessingClassList[] = $classData; + } + } + usort($dataProcessingClassList, function ($data1, $data2) { + return ((int)$data1['sortOrder'] > (int)$data2['sortOrder']) ? 1 : -1; + }); + $dataProcessingInstances = []; + foreach ($dataProcessingClassList as $classData) { + $dataProcessingInstances[] = $this->objectManager->get($classData['class']); + } + return $dataProcessingInstances; + } + + /** + * Validate hydrator or dehydrator classes and throw exception if class does not implement relevant interface. + * + * @param array $classChain + * @param string $interfaceName + * @param string $resolverClass + * @return void + * @throws ConfigurationMismatchException + */ + private function validateClassChain(array $classChain, string $interfaceName, string $resolverClass) + { + foreach ($classChain as $classData) { + if (!is_a($classData['class'], $interfaceName, true)) { + if ($interfaceName == HydratorInterface::class) { + throw new ConfigurationMismatchException( + __( + 'Hydrator %1 configured for resolver %2 must implement %3.', + $classData['class'], + $resolverClass, + $interfaceName + ) + ); + } else { + throw new ConfigurationMismatchException( + __( + 'Dehydrator %1 configured for resolver %2 must implement %3.', + $classData['class'], + $resolverClass, + $interfaceName + ) + ); + } + + } + } + } + + /** + * Get class inheritance chain for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array + */ + private function getResolverClassChain(ResolverInterface $resolver): array + { + $resolverClasses = [trim(get_class($resolver), '\\')]; + foreach (class_parents($resolver) as $classParent) { + $resolverClasses[] = trim($classParent, '\\'); + } + return $resolverClasses; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php new file mode 100644 index 0000000000000..95660f6ed5771 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Hydrator interface for resolver data. + */ +interface HydratorInterface +{ + /** + * Hydrate resolved data. + * + * @param array $resolverData + * @return void + */ + public function hydrate(array &$resolverData): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php new file mode 100644 index 0000000000000..9d1e1d6db7531 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * Interface for resolver-based hydrator provider. + */ +interface HydratorProviderInterface +{ + /** + * Returns hydrator for the given resolver, null if no hydrators configured. + * + * @param ResolverInterface $resolver + * + * @return HydratorInterface|null + */ + public function getHydratorForResolver(ResolverInterface $resolver) : ?HydratorInterface; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php new file mode 100644 index 0000000000000..4840643d2bf7c --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; + +class ResolverIdentityClassProvider +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * Map of Resolver Class Name => Identity Provider + * + * @var string[] + */ + private array $cacheableResolverClassNameIdentityMap; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $cacheableResolverClassNameIdentityMap + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $cacheableResolverClassNameIdentityMap + ) { + $this->objectManager = $objectManager; + $this->cacheableResolverClassNameIdentityMap = $cacheableResolverClassNameIdentityMap; + } + + /** + * Get Identity provider based on $resolver instance. + * + * @param ResolverInterface $resolver + * @return Cache\IdentityInterface|null + */ + public function getIdentityFromResolver(ResolverInterface $resolver): ?Cache\IdentityInterface + { + $matchingIdentityProviderClassName = null; + + foreach ($this->cacheableResolverClassNameIdentityMap as $resolverClassName => $identityProviderClassName) { + if ($resolver instanceof $resolverClassName) { + $matchingIdentityProviderClassName = $identityProviderClassName; + break; + } + } + + if (!$matchingIdentityProviderClassName) { + return null; + } + + return $this->objectManager->get($matchingIdentityProviderClassName); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php new file mode 100644 index 0000000000000..19e650a315c76 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\App\Cache\Tag\Resolver; +use Magento\Framework\App\Cache\Tag\Strategy\Factory as StrategyFactory; + +class TagResolver extends Resolver +{ + /** + * @var array + */ + private $invalidatableObjectTypes; + + /** + * GraphQL Resolver cache-specific tag resolver for the purpose of invalidation + * + * @param StrategyFactory $factory + * @param array $invalidatableObjectTypes + */ + public function __construct( + StrategyFactory $factory, + array $invalidatableObjectTypes = [] + ) { + $this->invalidatableObjectTypes = $invalidatableObjectTypes; + + parent::__construct($factory); + } + + /** + * @inheritdoc + */ + public function getTags($object) + { + $isInvalidatable = false; + + foreach ($this->invalidatableObjectTypes as $invalidatableObjectType) { + $isInvalidatable = $object instanceof $invalidatableObjectType; + + if ($isInvalidatable) { + break; + } + } + + if (!$isInvalidatable) { + return []; + } + + return parent::getTags($object); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php new file mode 100644 index 0000000000000..950ec9baeb417 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\App\Cache\Type\FrontendPool; +use Magento\Framework\Cache\Frontend\Decorator\TagScope; + +class Type extends TagScope +{ + /** + * Cache type code unique among all cache types + */ + public const TYPE_IDENTIFIER = 'graphql_query_resolver_result'; + + /** + * Cache tag used to distinguish the cache type from all other cache + */ + public const CACHE_TAG = 'GRAPHQL_QUERY_RESOLVER_RESULT'; + + /** + * @param FrontendPool $cacheFrontendPool + */ + public function __construct(FrontendPool $cacheFrontendPool) + { + parent::__construct($cacheFrontendPool->get(self::TYPE_IDENTIFIER), self::CACHE_TAG); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php new file mode 100644 index 0000000000000..f95dee244c958 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\FlagSetterInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\FlagGetterInterface; + +/** + * Value processor for cached resolver value. + */ +class ValueProcessor implements ValueProcessorInterface +{ + /** + * @var HydratorProviderInterface + */ + private HydratorProviderInterface $hydratorProvider; + + /** + * @var HydratorInterface[] + */ + private array $hydrators = []; + + /** + * @var array + */ + private array $processedValues = []; + + /** + * @var DehydratorProviderInterface + */ + private DehydratorProviderInterface $dehydratorProvider; + + /** + * @var array + */ + private array $typeConfig; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @var FlagGetterInterface + */ + private FlagGetterInterface $defaultFlagGetter; + + /** + * @var FlagSetterInterface + */ + private FlagSetterInterface $defaultFlagSetter; + + /** + * @param HydratorProviderInterface $hydratorProvider + * @param DehydratorProviderInterface $dehydratorProvider + * @param ObjectManagerInterface $objectManager + * @param FlagGetterInterface $defaultFlagGetter + * @param FlagSetterInterface $defaultFlagSetter + * @param array $typeConfig + */ + public function __construct( + HydratorProviderInterface $hydratorProvider, + DehydratorProviderInterface $dehydratorProvider, + ObjectManagerInterface $objectManager, + FlagGetterInterface $defaultFlagGetter, + FlagSetterInterface $defaultFlagSetter, + array $typeConfig = [] + ) { + $this->hydratorProvider = $hydratorProvider; + $this->dehydratorProvider = $dehydratorProvider; + $this->typeConfig = $typeConfig; + $this->objectManager = $objectManager; + $this->defaultFlagGetter = $defaultFlagGetter; + $this->defaultFlagSetter = $defaultFlagSetter; + } + + /** + * Get flag setter for the resolver return type. + * + * @param ResolveInfo $info + * @return FlagSetterInterface + */ + private function getFlagSetterForType(ResolveInfo $info): FlagSetterInterface + { + if (isset($this->typeConfig['setters'][get_class($info->returnType)])) { + return $this->objectManager->get( + $this->typeConfig['setters'][get_class($info->returnType)] + ); + } + return $this->defaultFlagSetter; + } + + /** + * @inheritdoc + */ + public function processCachedValueAfterLoad( + ResolveInfo $info, + ResolverInterface $resolver, + string $cacheKey, + &$value + ): void { + if ($value === null) { + return; + } + $hydrator = $this->hydratorProvider->getHydratorForResolver($resolver); + if ($hydrator) { + $this->hydrators[$cacheKey] = $hydrator; + $this->getFlagSetterForType($info)->setFlagOnValue($value, $cacheKey); + } + } + + /** + * @inheritdoc + */ + public function preProcessParentValue(array &$value): void + { + $this->hydrateData($value); + } + + /** + * Perform data hydration. + * + * @param array|null $value + * @return void + */ + private function hydrateData(&$value) + { + if ($value === null) { + return; + } + // the parent value is always a single object that contains currently resolved value + $reference = $this->defaultFlagGetter->getFlagFromValue($value) ?? null; + if (isset($reference['cacheKey']) && isset($reference['index'])) { + $cacheKey = $reference['cacheKey']; + $index = $reference['index']; + if ($cacheKey) { + if (isset($this->processedValues[$cacheKey][$index])) { + $value = $this->processedValues[$cacheKey][$index]; + } elseif (isset($this->hydrators[$cacheKey]) + && $this->hydrators[$cacheKey] instanceof HydratorInterface + ) { + $this->hydrators[$cacheKey]->hydrate($value); + $this->defaultFlagSetter->unsetFlagFromValue($value); + $this->processedValues[$cacheKey][$index] = $value; + } + } + } + } + + /** + * @inheritdoc + */ + public function preProcessValueBeforeCacheSave(ResolverInterface $resolver, &$value): void + { + $dehydrator = $this->dehydratorProvider->getDehydratorForResolver($resolver); + if ($dehydrator) { + $dehydrator->dehydrate($value); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php new file mode 100644 index 0000000000000..82d45f3ccb32d --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter; + +/** + * Get flag from value. + */ +interface FlagGetterInterface +{ + /** + * Get value processing flag. + * + * @param array $value + * @return array|null + */ + public function getFlagFromValue($value): ?array; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php new file mode 100644 index 0000000000000..179629864915f --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Single entity object structure flag getter. + */ +class SingleObject implements FlagGetterInterface +{ + /** + * @inheritdoc + */ + public function getFlagFromValue($value): ?array + { + return $value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] ?? null; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php new file mode 100644 index 0000000000000..86945db807dca --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +/** + * Sets a value processing flag on value and unsets flag from value. + */ +interface FlagSetterInterface +{ + /** + * Set the value processing flag on value. + * + * @param array $value + * @param string $flagValue + * @return void + */ + public function setFlagOnValue(&$value, string $flagValue): void; + + /** + * Unsets flag from value. + * + * @param array $value + * @return void + */ + public function unsetFlagFromValue(&$value): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php new file mode 100644 index 0000000000000..c4cd0165f15d6 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * List of objects value flag setter/unsetter. + */ +class ListOfObjects implements FlagSetterInterface +{ + /** + * @inheritdoc + */ + public function setFlagOnValue(&$value, string $flagValue): void + { + foreach (array_keys($value) as $key) { + $value[$key][ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] = [ + 'cacheKey' => $flagValue, + 'index' => $key + ]; + } + } + + /** + * @inheritdoc + */ + public function unsetFlagFromValue(&$value): void + { + foreach (array_keys($value) as $key) { + unset($value[$key][ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php new file mode 100644 index 0000000000000..bd860cd5cde18 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Single entity object flag value setter/unsetter. + */ +class SingleObject implements FlagSetterInterface +{ + /** + * @inheritdoc + */ + public function setFlagOnValue(&$value, string $flagValue): void + { + $value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] = [ + 'cacheKey' => $flagValue, + 'index' => 0 + ]; + } + + /** + * @inheritdoc + */ + public function unsetFlagFromValue(&$value): void + { + unset($value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php new file mode 100644 index 0000000000000..f2ce961f312d0 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Value processor for resolved value and parent resolver value. + */ +interface ValueProcessorInterface +{ + /** + * Key for data processing reference. + */ + public const VALUE_PROCESSING_REFERENCE_KEY = 'value_processing_reference_key'; + + /** + * Process the cached value after loading from cache for the given resolver. + * + * @param ResolveInfo $info + * @param ResolverInterface $resolver + * @param string $cacheKey + * @param array|mixed $value + * @return void + */ + public function processCachedValueAfterLoad( + ResolveInfo $info, + ResolverInterface $resolver, + string $cacheKey, + &$value + ): void; + + /** + * Preprocess parent resolver resolved array for currently executed array-element resolver. + * + * @param array $value + * @return void + */ + public function preProcessParentValue(array &$value): void; + + /** + * Preprocess value before saving to cache for the given resolver. + * + * @param ResolverInterface $resolver + * @param array|mixed $value + * @return void + */ + public function preProcessValueBeforeCacheSave(ResolverInterface $resolver, &$value): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php b/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php new file mode 100644 index 0000000000000..ab7abfc5ff718 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Observer; + +use Magento\Framework\App\Cache\StateInterface as CacheState; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; + +/** + * Invalidates graphql resolver result cache. + */ +class InvalidateGraphQlResolverCacheObserver implements ObserverInterface +{ + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var CacheState + */ + private $cacheState; + + /** + * @var TagResolver + */ + private $tagResolver; + + /** + * @param GraphQlResolverCache $graphQlResolverCache + * @param CacheState $cacheState + * @param TagResolver $tagResolver + */ + public function __construct( + GraphQlResolverCache $graphQlResolverCache, + CacheState $cacheState, + TagResolver $tagResolver + ) { + $this->graphQlResolverCache = $graphQlResolverCache; + $this->cacheState = $cacheState; + $this->tagResolver = $tagResolver; + } + + /** + * Clean identities of event object from GraphQL Resolver cache + * + * @param Observer $observer + * + * @return void + */ + public function execute(Observer $observer) + { + $object = $observer->getEvent()->getObject(); + + if (!is_object($object)) { + return; + } + + if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER)) { + return; + } + + $tags = $this->tagResolver->getTags($object); + + if (!empty($tags)) { + $this->graphQlResolverCache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/README.md b/app/code/Magento/GraphQlResolverCache/README.md new file mode 100644 index 0000000000000..e101723035b86 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/README.md @@ -0,0 +1,21 @@ +# Magento_GraphQlResolverCache module + +This module provides the ability to granular cache GraphQL resolver results on resolver level. + +## Installation + +Before installing this module, note that the Magento_GraphQlResolverCache module is dependent on the following modules: + +- `Magento_GraphQl` + +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). + +## Extensibility + +Extension developers can interact with the Magento_GraphQlResolverCache module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). + +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GraphQlCache module. + +## Additional information + +- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GraphQlResolverCache/composer.json b/app/code/Magento/GraphQlResolverCache/composer.json new file mode 100644 index 0000000000000..d73b69c86e707 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-graph-ql-resolver-cache", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GraphQlResolverCache\\": "" + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/etc/cache.xml b/app/code/Magento/GraphQlResolverCache/etc/cache.xml new file mode 100644 index 0000000000000..92667d350167a --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/cache.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Cache/etc/cache.xsd"> + <type name="graphql_query_resolver_result" translate="label,description" instance="Magento\GraphQlResolverCache\Model\Resolver\Result\Type"> + <label>GraphQL Query Resolver Results</label> + <description>Results from resolvers in GraphQL queries</description> + </type> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/etc/events.xml b/app/code/Magento/GraphQlResolverCache/etc/events.xml new file mode 100644 index 0000000000000..5633cd8b713de --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/events.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:Event/etc/events.xsd"> + <event name="clean_cache_by_tags"> + <observer name="invalidate_graphql_resolver_cache" instance="Magento\GraphQlResolverCache\Observer\InvalidateGraphQlResolverCacheObserver"/> + </event> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml new file mode 100644 index 0000000000000..060abbe09cf6f --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml @@ -0,0 +1,27 @@ +<?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\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider" /> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\FlagSetterInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\SingleObject"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\FlagGetterInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\SingleObject"/> + <type name="Magento\Framework\GraphQl\Query\ResolverInterface"> + <plugin name="cacheResolverResult" type="Magento\GraphQlResolverCache\Model\Plugin\Resolver\Cache" sortOrder="20"/> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor"> + <arguments> + <argument name="typeConfig" xsi:type="array"> + <item name="setters" xsi:type="array"> + <item name="Magento\Framework\GraphQl\Schema\Type\ListOfType" xsi:type="string">Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\ListOfObjects</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Elasticsearch8/etc/module.xml b/app/code/Magento/GraphQlResolverCache/etc/module.xml similarity index 69% rename from app/code/Magento/Elasticsearch8/etc/module.xml rename to app/code/Magento/GraphQlResolverCache/etc/module.xml index 32ea0b381b767..6639cd1c7f909 100644 --- a/app/code/Magento/Elasticsearch8/etc/module.xml +++ b/app/code/Magento/GraphQlResolverCache/etc/module.xml @@ -6,10 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Elasticsearch8"> + <module name="Magento_GraphQlResolverCache"> <sequence> - <module name="Magento_AdvancedSearch"/> - <module name="Magento_Elasticsearch"/> + <module name="Magento_GraphQl"/> </sequence> </module> </config> diff --git a/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv new file mode 100644 index 0000000000000..db3844a297f26 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv @@ -0,0 +1,4 @@ +"GraphQL Query Resolver Results","GraphQL Query Resolver Results" +"Results from resolvers in GraphQL queries","Results from resolvers in GraphQL queries" +"Hydrator %1 configured for resolver %2 must implement %3.","Hydrator %1 configured for resolver %2 must implement %3." +"Deydrator %1 configured for resolver %2 must implement %3.","Dehydrator %1 configured for resolver %2 must implement %3." diff --git a/app/code/Magento/GraphQlResolverCache/registration.php b/app/code/Magento/GraphQlResolverCache/registration.php new file mode 100644 index 0000000000000..e091fa98baaff --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_GraphQlResolverCache', __DIR__); diff --git a/app/code/Magento/GroupedImportExport/README.md b/app/code/Magento/GroupedImportExport/README.md index b092f88f421bd..fd055be68bdbe 100644 --- a/app/code/Magento/GroupedImportExport/README.md +++ b/app/code/Magento/GroupedImportExport/README.md @@ -16,5 +16,6 @@ Extension developers can interact with the Magento_GroupedImportExport module. F ## Additional information You can get more information about import/export processes in magento at the articles: + - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/GroupedProduct/README.md b/app/code/Magento/GroupedProduct/README.md index 2f141b463b19e..986b8f20791e8 100644 --- a/app/code/Magento/GroupedProduct/README.md +++ b/app/code/Magento/GroupedProduct/README.md @@ -11,12 +11,14 @@ This module extends the existing functionality of Magento_Catalog module by addi ## Installation details Before installing this module, note that the Magento_GroupedProduct module is dependent on the following modules: + - `Magento_Catalog` - `Magento_CatalogInventory` - `Magento_Sales` - `Magento_Quote` Before disabling or uninstalling this module, note that the following modules depends on this module: + - `Magento_GroupedCatalogInventory` - `Magento_GroupedProductGraphQl` - `Magento_MsrpGroupedProduct` @@ -38,6 +40,7 @@ Extension developers can interact with the Magento_GroupedProduct module. For mo ### Layouts This module introduces the following layouts in the `view/frontend/layout`, `view/adminhtml/layout` and `view/base/layout` directories: + - `view/adminhtml/layout`: - `catalog_product_grouped` - `catalog_product_new` @@ -73,9 +76,11 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components You can extend a grouped product listing updates using the configuration files located in the `view/adminhtml/ui_component` directory: + - `grouped_product_listing` This module extends widgets ui components the configuration files located in the `view/frontend/ui_component` directory: + - `widget_recently_compared` - `widget_recently_viewed` @@ -85,7 +90,7 @@ For information about a UI component in Magento 2, see [Overview of UI component - `\Magento\GroupedProduct\Api\Data\GroupedOptionsInterface` - represents `product item id with qty` of a grouped product - + For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 4b79f8f109839..480ae502cc8f0 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-106"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml index d5dcd7f48b956..456982014c609 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-39950"/> <severity value="MAJOR"/> <group value="groupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml index 55144884c7195..610554d0d0f8c 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-26602"/> <severity value="MAJOR"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- creating category, simple products --> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml index 68f9da93ec992..2980a6cefa54c 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-11019"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml index ef1665d965200..16c4c29c18a69 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-93181"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml index 8d808cd07a875..c1ef2864bb8be 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-3755"/> <group value="GroupedProduct"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before></before> <after> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml index 0dc622a82aaae..9d33ca36bd48d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-198"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml index b51b14f254099..cbef10f3da280 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-146"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php index f31b3a3db9d8a..94331fc65278c 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php @@ -349,7 +349,7 @@ protected function setUp(): void */ public function testGetProductLinks(): void { - $this->markTestIncomplete('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926'); + $this->markTestSkipped('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926'); $linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3]; $this->linkTypeProviderMock->expects($this->once())->method('getLinkTypes')->willReturn($linkTypes); diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php index b2336a0741292..3a45f7a234799 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -9,6 +9,7 @@ use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\SaleableInterface; @@ -17,7 +18,7 @@ /** * Provides product prices for configurable products */ -class Provider implements ProviderInterface +class Provider implements ProviderInterface, ResetAfterRequestInterface { /** * Cache product prices so only fetch once @@ -93,4 +94,12 @@ private function getMinimalProductAmount(SaleableInterface $product, string $pri return $this->minimalProductAmounts[$product->getId()][$priceType]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->minimalProductAmounts = []; + } } diff --git a/app/code/Magento/GroupedProductGraphQl/README.md b/app/code/Magento/GroupedProductGraphQl/README.md index 07ac1f2cecf98..f29f3098ae033 100644 --- a/app/code/Magento/GroupedProductGraphQl/README.md +++ b/app/code/Magento/GroupedProductGraphQl/README.md @@ -21,4 +21,4 @@ Extension developers can interact with the Magento_GroupedProductGraphQll module ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php index 9dcb2fdafb74f..6fd229f26a1a6 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\ImportExport\Model\Import; /** * Download history controller @@ -47,6 +48,7 @@ public function __construct( */ public function execute() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = basename($this->getRequest()->getParam('filename')); /** @var \Magento\ImportExport\Helper\Report $reportHelper */ @@ -59,17 +61,12 @@ public function execute() return $resultRedirect; } - $this->fileFactory->create( + return $this->fileFactory->create( $fileName, - null, + ['type' => 'filename', 'value' => Import::IMPORT_HISTORY_DIR . $fileName], DirectoryList::VAR_IMPORT_EXPORT, 'application/octet-stream', $reportHelper->getReportSize($fileName) ); - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($reportHelper->getReportOutput($fileName)); - return $resultRaw; } } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php index ebf88e6c68e23..b74f48685feed 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php @@ -106,18 +106,13 @@ public function execute() $fileSize = $this->sampleFileProvider->getSize($entityName); $fileName = $entityName . '.csv'; - $this->fileFactory->create( + return $this->fileFactory->create( $fileName, - null, + $fileContents, DirectoryList::VAR_IMPORT_EXPORT, 'application/octet-stream', $fileSize ); - - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($fileContents); - - return $resultRaw; } /** diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php index 4092879e23622..81347ce41a904 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php @@ -5,39 +5,43 @@ */ namespace Magento\ImportExport\Controller\Adminhtml; -use Magento\Backend\App\Action; -use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\Backend\App\Action\Context; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\History as ModelHistory; use Magento\Framework\Escaper; use Magento\Framework\App\ObjectManager; +use Magento\ImportExport\Model\Import\RenderErrorMessages; +use Magento\ImportExport\Model\Report\ReportProcessorInterface; /** * Import controller */ abstract class ImportResult extends Import { - const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download'; + public const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download'; /** * Limit view errors */ - const LIMIT_ERRORS_MESSAGE = 100; + public const LIMIT_ERRORS_MESSAGE = 100; /** - * @var \Magento\ImportExport\Model\Report\ReportProcessorInterface + * @var ReportProcessorInterface */ - protected $reportProcessor; + protected ReportProcessorInterface $reportProcessor; /** - * @var \Magento\ImportExport\Model\History + * @var ModelHistory */ - protected $historyModel; + protected ModelHistory $historyModel; /** - * @var \Magento\ImportExport\Helper\Report + * @var Report */ - protected $reportHelper; + protected Report $reportHelper; /** * @var Escaper|null @@ -45,18 +49,25 @@ abstract class ImportResult extends Import protected $escaper; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor - * @param \Magento\ImportExport\Model\History $historyModel - * @param \Magento\ImportExport\Helper\Report $reportHelper + * @var RenderErrorMessages + */ + private RenderErrorMessages $renderErrorMessages; + + /** + * @param Context $context + * @param ReportProcessorInterface $reportProcessor + * @param ModelHistory $historyModel + * @param Report $reportHelper * @param Escaper|null $escaper + * @param RenderErrorMessages|null $renderErrorMessages */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor, - \Magento\ImportExport\Model\History $historyModel, - \Magento\ImportExport\Helper\Report $reportHelper, - Escaper $escaper = null + Context $context, + ReportProcessorInterface $reportProcessor, + ModelHistory $historyModel, + Report $reportHelper, + Escaper $escaper = null, + ?RenderErrorMessages $renderErrorMessages = null ) { parent::__construct($context); $this->reportProcessor = $reportProcessor; @@ -64,46 +75,25 @@ public function __construct( $this->reportHelper = $reportHelper; $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); + $this->renderErrorMessages = $renderErrorMessages ?? + ObjectManager::getInstance()->get(RenderErrorMessages::class); } /** * Add Error Messages for Import * - * @param \Magento\Framework\View\Element\AbstractBlock $resultBlock + * @param AbstractBlock $resultBlock * @param ProcessingErrorAggregatorInterface $errorAggregator * @return $this */ protected function addErrorMessages( - \Magento\Framework\View\Element\AbstractBlock $resultBlock, + AbstractBlock $resultBlock, ProcessingErrorAggregatorInterface $errorAggregator ) { if ($errorAggregator->getErrorsCount()) { - $message = ''; - $counter = 0; - $escapedMessages = []; - foreach ($this->getErrorMessages($errorAggregator) as $error) { - $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error); - if ($counter >= self::LIMIT_ERRORS_MESSAGE) { - break; - } - } - if ($errorAggregator->hasFatalExceptions()) { - foreach ($this->getSystemExceptions($errorAggregator) as $error) { - $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage()) - . ' <a href="#" onclick="$(this).next().show();$(this).hide();return false;">' - . __('Show more') . '</a><div style="display:none;">' . __('Additional data') . ': ' - . $this->escaper->escapeHtml($error->getErrorDescription()) . '</div>'; - } - } try { - $message .= implode('<br>', $escapedMessages); $resultBlock->addNotice( - '<strong>' . __('Following Error(s) has been occurred during importing process:') . '</strong><br>' - . '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') - . '<a href="' - . $this->createDownloadUrlImportHistoryFile($this->createErrorReport($errorAggregator)) - . '">' . __('Download full report') . '</a><br>' - . '<div class="import-error-list">' . $message . '</div></div>' + $this->renderErrorMessages->renderMessages($errorAggregator) ); } catch (\Exception $e) { foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) { @@ -118,28 +108,23 @@ protected function addErrorMessages( /** * Get all Error Messages from Import Results * - * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface $errorAggregator + * @param ProcessingErrorAggregatorInterface $errorAggregator * @return array */ protected function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator) { - $messages = []; - $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); - foreach ($rowMessages as $errorCode => $rows) { - $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows); - } - return $messages; + return $this->renderErrorMessages->getErrorMessages($errorAggregator); } /** * Get System Generated Exception * * @param ProcessingErrorAggregatorInterface $errorAggregator - * @return \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] + * @return ProcessingError[] */ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator) { - return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + return $this->renderErrorMessages->getSystemExceptions($errorAggregator); } /** @@ -150,15 +135,7 @@ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $error */ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator) { - $this->historyModel->loadLastInsertItem(); - $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile()); - $writeOnlyErrorItems = true; - if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) { - $writeOnlyErrorItems = false; - } - $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems); - $this->historyModel->addErrorReportFile($fileName); - return $fileName; + return $this->renderErrorMessages->createErrorReport($errorAggregator); } /** @@ -169,6 +146,6 @@ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAg */ protected function createDownloadUrlImportHistoryFile($fileName) { - return $this->getUrl(self::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]); + return $this->renderErrorMessages->createDownloadUrlImportHistoryFile($fileName); } } diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 6d466ba6dcdb8..1252a0665009b 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -6,6 +6,12 @@ namespace Magento\ImportExport\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Export\ConfigInterface; +use Magento\ImportExport\Model\Export\Entity\Factory; +use Psr\Log\LoggerInterface; + /** * Export model * @@ -78,12 +84,18 @@ class Export extends \Magento\ImportExport\Model\AbstractModel ]; /** - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig - * @param \Magento\ImportExport\Model\Export\Entity\Factory $entityFactory + * @var LocaleEmulatorInterface + */ + private $localeEmulator; + + /** + * @param LoggerInterface $logger + * @param Filesystem $filesystem + * @param ConfigInterface $exportConfig + * @param Factory $entityFactory * @param \Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac * @param array $data + * @param LocaleEmulatorInterface|null $localeEmulator */ public function __construct( \Psr\Log\LoggerInterface $logger, @@ -91,12 +103,14 @@ public function __construct( \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig, \Magento\ImportExport\Model\Export\Entity\Factory $entityFactory, \Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac, - array $data = [] + array $data = [], + ?LocaleEmulatorInterface $localeEmulator = null ) { $this->_exportConfig = $exportConfig; $this->_entityFactory = $entityFactory; $this->_exportAdapterFac = $exportAdapterFac; parent::__construct($logger, $filesystem, $data); + $this->localeEmulator = $localeEmulator ?? ObjectManager::getInstance()->get(LocaleEmulatorInterface::class); } /** @@ -188,6 +202,20 @@ protected function _getWriter() * @throws \Magento\Framework\Exception\LocalizedException */ public function export() + { + return $this->localeEmulator->emulate( + $this->exportCallback(...), + $this->getData('locale') ?: null + ); + } + + /** + * Export data. + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function exportCallback() { if (isset($this->_data[self::FILTER_ELEMENT_GROUP])) { $this->addLogComment(__('Begin export of %1', $this->getEntity())); diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php index e83f508037da1..7623677a4781d 100644 --- a/app/code/Magento/ImportExport/Model/Export/Consumer.php +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -11,7 +11,6 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; -use Magento\Framework\Locale\ResolverInterface; use Magento\ImportExport\Api\Data\LocalizedExportInfoInterface; use Magento\ImportExport\Api\ExportManagementInterface; use Magento\Framework\Notification\NotifierInterface; @@ -41,31 +40,23 @@ class Consumer */ private $filesystem; - /** - * @var ResolverInterface - */ - private $localeResolver; - /** * Consumer constructor. * @param \Psr\Log\LoggerInterface $logger * @param ExportManagementInterface $exportManager * @param Filesystem $filesystem * @param NotifierInterface $notifier - * @param ResolverInterface $localeResolver */ public function __construct( \Psr\Log\LoggerInterface $logger, ExportManagementInterface $exportManager, Filesystem $filesystem, - NotifierInterface $notifier, - ResolverInterface $localeResolver + NotifierInterface $notifier ) { $this->logger = $logger; $this->exportManager = $exportManager; $this->filesystem = $filesystem; $this->notifier = $notifier; - $this->localeResolver = $localeResolver; } /** @@ -76,11 +67,6 @@ public function __construct( */ public function process(LocalizedExportInfoInterface $exportInfo) { - $currentLocale = $this->localeResolver->getLocale(); - if ($exportInfo->getLocale()) { - $this->localeResolver->setLocale($exportInfo->getLocale()); - } - try { $data = $this->exportManager->export($exportInfo); $fileName = $exportInfo->getFileName(); @@ -97,8 +83,6 @@ public function process(LocalizedExportInfoInterface $exportInfo) __('Error during export process occurred. Please check logs for detail') ); $this->logger->critical('Something went wrong while export process. ' . $exception->getMessage()); - } finally { - $this->localeResolver->setLocale($currentLocale); } } } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php index d9dd98bc54cd6..3e0b403089ac2 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php @@ -286,7 +286,8 @@ protected function _addAttributeValuesToRow(\Magento\Framework\Model\AbstractMod if ($this->isMultiselect($attributeCode)) { $values = []; - $attributeValue = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue); + $attributeValue = + $attributeValue ? explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue) : []; foreach ($attributeValue as $value) { $values[] = $this->getAttributeValueById($attributeCode, $value); } diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index aa3af449237f9..f86722895a406 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -210,18 +210,23 @@ class Import extends AbstractModel */ private $upload; + /** + * @var LocaleEmulatorInterface + */ + private $localeEmulator; + /** * @param LoggerInterface $logger * @param Filesystem $filesystem * @param DataHelper $importExportData * @param ScopeConfigInterface $coreConfig - * @param Import\ConfigInterface $importConfig - * @param Import\Entity\Factory $entityFactory + * @param ConfigInterface $importConfig + * @param Factory $entityFactory * @param Data $importData - * @param Export\Adapter\CsvFactory $csvFactory + * @param CsvFactory $csvFactory * @param FileTransferFactory $httpFactory * @param UploaderFactory $uploaderFactory - * @param Source\Import\Behavior\Factory $behaviorFactory + * @param Factory $behaviorFactory * @param IndexerRegistry $indexerRegistry * @param History $importHistoryModel * @param DateTime $localeDate @@ -229,6 +234,7 @@ class Import extends AbstractModel * @param ManagerInterface|null $messageManager * @param Random|null $random * @param Upload|null $upload + * @param LocaleEmulatorInterface|null $localeEmulator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -249,7 +255,8 @@ public function __construct( array $data = [], ManagerInterface $messageManager = null, Random $random = null, - Upload $upload = null + Upload $upload = null, + LocaleEmulatorInterface $localeEmulator = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -270,16 +277,36 @@ public function __construct( ->get(Random::class); $this->upload = $upload ?: ObjectManager::getInstance() ->get(Upload::class); + $this->localeEmulator = $localeEmulator ?: ObjectManager::getInstance() + ->get(LocaleEmulatorInterface::class); parent::__construct($logger, $filesystem, $data); } /** - * Create instance of entity adapter and return it + * Returns or create existing instance of entity adapter * * @throws LocalizedException * @return EntityInterface */ protected function _getEntityAdapter() + { + if (!$this->_entityAdapter) { + $this->_entityAdapter = $this->localeEmulator->emulate( + $this->createEntityAdapter(...), + $this->getData('locale') ?: null + ); + } + + return $this->_entityAdapter; + } + + /** + * Create instance of entity adapter and return it + * + * @throws LocalizedException + * @return EntityInterface + */ + private function createEntityAdapter() { if (!$this->_entityAdapter) { $entities = $this->_importConfig->getEntities(); @@ -479,6 +506,20 @@ public function getWorkingDir() * @throws LocalizedException */ public function importSource() + { + return $this->localeEmulator->emulate( + $this->importSourceCallback(...), + $this->getData('locale') ?: null + ); + } + + /** + * Import source file structure to DB. + * + * @return bool + * @throws LocalizedException + */ + private function importSourceCallback() { $ids = $this->_getEntityAdapter()->getIds(); if (empty($ids)) { @@ -629,6 +670,21 @@ protected function _removeBom($sourceFile) return $this; } + /** + * Validates source file and returns validation result + * + * @param AbstractSource $source + * @return bool + * @throws LocalizedException + */ + public function validateSource(AbstractSource $source) + { + return $this->localeEmulator->emulate( + fn () => $this->validateSourceCallback($source), + $this->getData('locale') ?: null + ); + } + /** * Validates source file and returns validation result * @@ -639,7 +695,7 @@ protected function _removeBom($sourceFile) * @return bool * @throws LocalizedException */ - public function validateSource(AbstractSource $source) + private function validateSourceCallback(AbstractSource $source) { $this->addLogComment(__('Begin data validation')); diff --git a/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php new file mode 100644 index 0000000000000..cb163edb55ce5 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\History as ModelHistory; +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\Report\ReportProcessorInterface; +use Magento\ImportExport\Controller\Adminhtml\ImportResult; + +/** + * Import Render Error Messages Service model. + */ +class RenderErrorMessages +{ + /** + * @var ReportProcessorInterface + */ + private ReportProcessorInterface $reportProcessor; + + /** + * @var ModelHistory + */ + private ModelHistory $historyModel; + + /** + * @var Report + */ + private Report $reportHelper; + + /** + * @var Escaper|mixed + */ + private mixed $escaper; + + /** + * @var UrlInterface + */ + private mixed $backendUrl; + + /** + * @param ReportProcessorInterface $reportProcessor + * @param ModelHistory $historyModel + * @param Report $reportHelper + * @param Escaper|null $escaper + * @param UrlInterface|null $backendUrl + */ + public function __construct( + ReportProcessorInterface $reportProcessor, + ModelHistory $historyModel, + Report $reportHelper, + ?Escaper $escaper = null, + ?UrlInterface $backendUrl = null + ) { + $this->reportProcessor = $reportProcessor; + $this->historyModel = $historyModel; + $this->reportHelper = $reportHelper; + $this->escaper = $escaper + ?? ObjectManager::getInstance()->get(Escaper::class); + $this->backendUrl = $backendUrl + ?? ObjectManager::getInstance()->get(UrlInterface::class); + } + + /** + * Add Error Messages for Import + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + public function renderMessages( + ProcessingErrorAggregatorInterface $errorAggregator + ): string { + $message = ''; + $counter = 0; + $escapedMessages = []; + foreach ($this->getErrorMessages($errorAggregator) as $error) { + $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error); + if ($counter >= ImportResult::LIMIT_ERRORS_MESSAGE) { + break; + } + } + if ($errorAggregator->hasFatalExceptions()) { + foreach ($this->getSystemExceptions($errorAggregator) as $error) { + $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage()) + . ' <a href="#" onclick="$(this).next().show();$(this).hide();return false;">' + . __('Show more') . '</a><div style="display:none;">' . __('Additional data') . ': ' + . $this->escaper->escapeHtml($error->getErrorDescription()) . '</div>'; + } + } + $message .= implode('<br>', $escapedMessages); + return '<strong>' . __('Following Error(s) has been occurred during importing process:') . '</strong><br>' + . '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') + . '<a href="' + . $this->createDownloadUrlImportHistoryFile($this->createErrorReport($errorAggregator)) + . '">' . __('Download full report') . '</a><br>' + . '<div class="import-error-list">' . $message . '</div></div>'; + } + + /** + * Get all Error Messages from Import Results + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return array + */ + public function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator): array + { + $messages = []; + $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + foreach ($rowMessages as $errorCode => $rows) { + $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows); + } + return $messages; + } + + /** + * Get System Generated Exception + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return ProcessingError[] + */ + public function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator): array + { + return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + } + + /** + * Generate Error Report File + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + public function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator): string + { + $this->historyModel->loadLastInsertItem(); + $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile()); + $writeOnlyErrorItems = true; + if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) { + $writeOnlyErrorItems = false; + } + $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems); + $this->historyModel->addErrorReportFile($fileName); + return $fileName; + } + + /** + * Get Import History Url + * + * @param string $fileName + * @return string + */ + public function createDownloadUrlImportHistoryFile($fileName): string + { + return $this->backendUrl->getUrl(ImportResult::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]); + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php index 71780d8ae8b0e..178ca38ede0ae 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php @@ -55,8 +55,6 @@ public function __construct( $delimiter = ',', $enclosure = '"' ) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - register_shutdown_function([$this, 'destruct']); if ($file instanceof FileReadInterface) { $this->filePath = ''; $this->_file = $file; @@ -83,7 +81,7 @@ public function __construct( * * @return void */ - public function destruct() + public function __destruct() { if (is_object($this->_file) && !empty(self::$openFiles[$this->filePath])) { $this->_file->close(); diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulator.php b/app/code/Magento/ImportExport/Model/LocaleEmulator.php new file mode 100644 index 0000000000000..48e781c505d0c --- /dev/null +++ b/app/code/Magento/ImportExport/Model/LocaleEmulator.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\TranslateInterface; + +class LocaleEmulator implements LocaleEmulatorInterface +{ + /** + * @var bool + */ + private bool $isEmulating = false; + + /** + * @param TranslateInterface $translate + * @param RendererInterface $phraseRenderer + * @param ResolverInterface $localeResolver + * @param ResolverInterface $defaultLocaleResolver + */ + public function __construct( + private readonly TranslateInterface $translate, + private readonly RendererInterface $phraseRenderer, + private readonly ResolverInterface $localeResolver, + private readonly ResolverInterface $defaultLocaleResolver + ) { + } + + /** + * @inheritdoc + */ + public function emulate(callable $callback, ?string $locale = null): mixed + { + if ($this->isEmulating) { + return $callback(); + } + $this->isEmulating = true; + $locale ??= $this->defaultLocaleResolver->getLocale(); + $initialLocale = $this->localeResolver->getLocale(); + $initialPhraseRenderer = Phrase::getRenderer(); + Phrase::setRenderer($this->phraseRenderer); + $this->localeResolver->setLocale($locale); + $this->translate->setLocale($locale); + $this->translate->loadData(); + try { + $result = $callback(); + } finally { + Phrase::setRenderer($initialPhraseRenderer); + $this->localeResolver->setLocale($initialLocale); + $this->translate->setLocale($initialLocale); + $this->translate->loadData(); + $this->isEmulating = false; + } + return $result; + } +} diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php new file mode 100644 index 0000000000000..ab0743230e6e9 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model; + +/** + * Locale emulator for import and export + */ +interface LocaleEmulatorInterface +{ + /** + * Emulates given $locale during execution of $callback + * + * @param callable $callback + * @param string|null $locale + * @return mixed + */ + public function emulate(callable $callback, ?string $locale = null): mixed; +} diff --git a/app/code/Magento/ImportExport/Model/Source/Upload.php b/app/code/Magento/ImportExport/Model/Source/Upload.php index fdddaaf1a4abe..c50d079e895e5 100644 --- a/app/code/Magento/ImportExport/Model/Source/Upload.php +++ b/app/code/Magento/ImportExport/Model/Source/Upload.php @@ -7,6 +7,8 @@ namespace Magento\ImportExport\Model\Source; +use Laminas\File\Transfer\Adapter\Http; +use Laminas\Validator\File\Upload as FileUploadValidator; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; @@ -74,11 +76,13 @@ public function __construct( */ public function uploadSource(string $entity) { - /** @var $adapter \Zend_File_Transfer_Adapter_Http */ + /** + * @var $adapter Http + */ $adapter = $this->httpFactory->create(); if (!$adapter->isValid(Import::FIELD_NAME_SOURCE_FILE)) { $errors = $adapter->getErrors(); - if ($errors[0] == \Zend_Validate_File_Upload::INI_SIZE) { + if ($errors[0] == FileUploadValidator::INI_SIZE) { $errorMessage = $this->importExportData->getMaxUploadSizeMessage(); } else { $errorMessage = __('The file was not uploaded.'); @@ -86,7 +90,9 @@ public function uploadSource(string $entity) throw new LocalizedException($errorMessage); } - /** @var $uploader Uploader */ + /** + * @var $uploader Uploader + */ $uploader = $this->uploaderFactory->create(['fileId' => Import::FIELD_NAME_SOURCE_FILE]); $uploader->setAllowedExtensions(['csv', 'zip']); $uploader->skipDbProcessing(true); diff --git a/app/code/Magento/ImportExport/README.md b/app/code/Magento/ImportExport/README.md index ef1a9acbcce0f..a7a395c291cbe 100644 --- a/app/code/Magento/ImportExport/README.md +++ b/app/code/Magento/ImportExport/README.md @@ -1,4 +1,4 @@ -# Magento_ImportExport module +# Magento_ImportExport module This module provides a framework and basic functionality for importing/exporting various entities in Magento. It can be disabled and in such case all dependent import/export functionality (products, customers, orders etc.) will be disabled in Magento. @@ -6,6 +6,7 @@ It can be disabled and in such case all dependent import/export functionality (p ## Installation The Magento_ImportExport module creates the following tables in the database: + - `importexport_importdata` - `import_history` @@ -44,7 +45,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] You can extend an export updates using the configuration files located in the `view/adminhtml/ui_component` directory: -- `export_grid` +- `export_grid` For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). @@ -80,6 +81,7 @@ For information about a public API in Magento 2, see [Public interfaces & APIs]( 2. Create an export model You can get more information about import/export processes in magento at the articles: + - [Create custom import entity](https://developer.adobe.com/commerce/php/tutorials/backend/create-custom-import-entity/) - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml index d2eee7c3c5f42..d2b10d3d541b1 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml index 361722ec10b9b..4c547e17f1c23 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml index 9f286d5148a08..1a8f993b5e3c3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml index b9039fa59f5cb..a06e9b61bea9a 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-91569"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml index 8d405d7813cc9..fbfd966eaac93 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14077"/> <group value="importExport"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product1 --> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml index 503037401b9f7..e076931785313 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-30587"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <!--Create Simple product--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml index 5fe42e7074031..a4a49118d001f 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-65066"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Login to Admin Page--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml index 95a6a453e1e06..f3a36e6fef704 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14076"/> <group value="importExport"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product2 --> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml index 0403649d7add5..cf64852c7b50e 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-70803"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml index 3a4bd2507e8b6..b7572f359ccf6 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6406"/> <useCaseId value="MAGETWO-59265"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml index 2811852fefaf4..ec03c03d052a3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml index 6c2d7f76cce32..9910c5f91886f 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6317"/> <useCaseId value="MAGETWO-91544"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <!--Create Product--> diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php index cf69cdf7ee367..a7b1501e32eaa 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php @@ -333,6 +333,6 @@ private function getAttributeMock(array $data): Attribute */ public function testPrepareForm() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php index d153c169bfdd0..d31d22a97a380 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php @@ -83,6 +83,6 @@ protected function setUp(): void */ public function testPrepareForm() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php index 57e33a1dd51a3..7c8e06d3f681d 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php @@ -9,14 +9,17 @@ use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Redirect; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Controller\Result\RedirectFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\ImportExport\Controller\Adminhtml\History\Download; use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\Import; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -155,8 +158,20 @@ public function testExecute($requestFilename, $processedFilename) $this->reportHelper->method('importFileExists') ->with($processedFilename) ->willReturn(true); - $this->resultRaw->expects($this->once())->method('setContents'); - $this->downloadController->execute(); + + $responseMock = $this->getMockBuilder(ResponseInterface::class) + ->getMock(); + $this->fileFactory->expects($this->once()) + ->method('create') + ->with( + $processedFilename, + ['type' => 'filename', 'value' =>Import::IMPORT_HISTORY_DIR . $processedFilename], + DirectoryList::VAR_IMPORT_EXPORT, + 'application/octet-stream', + 1 + ) + ->willReturn($responseMock); + $this->assertSame($responseMock, $this->downloadController->execute()); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php index 2f10ce42f84d4..1a5677c555b1b 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php @@ -27,6 +27,7 @@ use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\Config; use Magento\ImportExport\Model\Import\Entity\Factory; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use Magento\ImportExport\Model\Source\Upload; use Magento\MediaStorage\Model\File\UploaderFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -153,7 +154,7 @@ protected function setUp(): void */ public function testGetExecutionTime() { - $this->markTestIncomplete('Invalid mocks used for DateTime object. Investigate later.'); + $this->markTestSkipped('Invalid mocks used for DateTime object. Investigate later.'); $startDate = '2000-01-01 01:01:01'; $endDate = '2000-01-01 02:03:04'; @@ -204,6 +205,9 @@ public function testGetSummaryStats() $importHistoryModel = $this->createMock(History::class); $localeDate = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class); $upload = $this->createMock(Upload::class); + $localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); + $localeEmulator->method('emulate') + ->willReturnCallback(fn (callable $callback) => $callback()); $import = new Import( $logger, $filesystem, @@ -222,7 +226,8 @@ public function testGetSummaryStats() [], null, null, - $upload + $upload, + $localeEmulator ); $import->setData('entity', 'catalog_product'); $message = $this->report->getSummaryStats($import); diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php index abef693c9acad..815680df29083 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php @@ -41,11 +41,6 @@ class ConsumerTest extends TestCase */ private $notifierMock; - /** - * @var ResolverInterface|MockObject - */ - private $localeResolver; - /** * @var Consumer */ @@ -57,36 +52,21 @@ protected function setUp(): void $this->exportManagementMock = $this->createMock(ExportManagementInterface::class); $this->filesystemMock = $this->createMock(Filesystem::class); $this->notifierMock = $this->createMock(NotifierInterface::class); - $this->localeResolver = $this->createMock(ResolverInterface::class); $this->consumer = new Consumer( $this->loggerMock, $this->exportManagementMock, $this->filesystemMock, - $this->notifierMock, - $this->localeResolver + $this->notifierMock ); } public function testProcess() { - $adminLocale = 'de_DE'; $exportInfoMock = $this->createMock(LocalizedExportInfoInterface::class); - $exportInfoMock->expects($this->atLeastOnce()) - ->method('getLocale') - ->willReturn($adminLocale); $exportInfoMock->expects($this->atLeastOnce()) ->method('getFileName') ->willReturn('file_name.csv'); - $defaultLocale = 'en_US'; - $this->localeResolver->expects($this->once()) - ->method('getLocale') - ->willReturn($defaultLocale); - $this->localeResolver->expects($this->exactly(2)) - ->method('setLocale') - ->withConsecutive([$adminLocale], [$defaultLocale]) - ->willReturn($this->localeResolver); - $data = '1,2,3'; $this->exportManagementMock->expects($this->once()) ->method('export') diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php index 03c6356fc1adc..40e9191c17bda 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php @@ -16,6 +16,7 @@ use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter; use Magento\ImportExport\Model\Export\ConfigInterface; use Magento\ImportExport\Model\Export\Entity\Factory; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -23,87 +24,93 @@ class ExportTest extends TestCase { /** - * Extension for export file - * - * @var string + * @var ConfigInterface|MockObject */ - protected $_exportFileExtension = 'csv'; + private $exportConfigMock; /** - * @var MockObject + * @var AbstractEntity|MockObject */ - protected $_exportConfigMock; + private $exportAbstractEntityMock; /** - * @var AbstractEntity|MockObject + * @var AbstractAdapter|MockObject */ - private $abstractMockEntity; + private $exportAdapterMock; /** - * Return mock for \Magento\ImportExport\Model\Export class - * - * @return Export + * @var Export */ - protected function _getMageImportExportModelExportMock() - { - $this->_exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); + private $model; - $this->abstractMockEntity = $this->getMockForAbstractClass( - AbstractEntity::class, - [], - '', - false - ); + /** + * @var string[] + */ + private $entities = [ + 'entityA' => [ + 'model' => 'entityAClass' + ], + 'entityB' => [ + 'model' => 'entityBClass' + ] + ]; + /** + * @var string[] + */ + private $fileFormats = [ + 'csv' => [ + 'model' => 'csvFormatClass' + ], + 'xml' => [ + 'model' => 'xmlFormatClass' + ] + ]; - /** @var $mockAdapterTest \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter */ - $mockAdapterTest = $this->getMockForAbstractClass( - AbstractAdapter::class, - [], - '', - false, - true, - true, - ['getFileExtension'] - ); - $mockAdapterTest->expects( - $this->any() - )->method( - 'getFileExtension' - )->willReturn( - $this->_exportFileExtension - ); + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulator; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); + $this->exportConfigMock->method('getEntities') + ->willReturn($this->entities); + $this->exportConfigMock->method('getFileFormats') + ->willReturn($this->fileFormats); + + $this->exportAbstractEntityMock = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->exportAdapterMock = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFileExtension']) + ->getMockForAbstractClass(); $logger = $this->getMockForAbstractClass(LoggerInterface::class); $filesystem = $this->createMock(Filesystem::class); $entityFactory = $this->createMock(Factory::class); - $exportAdapterFac = $this->createMock(\Magento\ImportExport\Model\Export\Adapter\Factory::class); - /** @var \Magento\ImportExport\Model\Export $mockModelExport */ - $mockModelExport = $this->getMockBuilder(Export::class) - ->setMethods(['getEntityAdapter', '_getEntityAdapter', '_getWriter', 'setWriter']) - ->setConstructorArgs([$logger, $filesystem, $this->_exportConfigMock, $entityFactory, $exportAdapterFac]) - ->getMock(); - $mockModelExport->expects( - $this->any() - )->method( - 'getEntityAdapter' - )->willReturn( - $this->abstractMockEntity - ); - $mockModelExport->expects( - $this->any() - )->method( - '_getEntityAdapter' - )->willReturn( - $this->abstractMockEntity - ); - $mockModelExport->method( - 'setWriter' - )->willReturn( - $this->abstractMockEntity + $entityFactory->method('create') + ->willReturn($this->exportAbstractEntityMock); + $exportAdapterFac = $this->createMock(Export\Adapter\Factory::class); + $exportAdapterFac->method('create') + ->willReturn($this->exportAdapterMock); + $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); + + $this->model = new Export( + $logger, + $filesystem, + $this->exportConfigMock, + $entityFactory, + $exportAdapterFac, + [], + $this->localeEmulator ); - $mockModelExport->expects($this->any())->method('_getWriter')->willReturn($mockAdapterTest); - - return $mockModelExport; } /** @@ -114,17 +121,28 @@ protected function _getMageImportExportModelExportMock() */ public function testExportDoesntTrimResult() { - $model = $this->_getMageImportExportModelExportMock(); - $this->abstractMockEntity->method('export') - ->willReturn("export data \n\n"); - $model->setData([ + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', Export::FILTER_ELEMENT_GROUP => [], - 'entity' => 'catalog_product' - ]); - $model->export(); + 'locale' => $locale + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + + $this->exportAbstractEntityMock->method('export') + ->willReturn("export data \n\n"); + $this->model->export(); $this->assertStringContainsString( 'Exported 2 rows', - var_export($model->getFormatedLogTrace(), true) + var_export($this->model->getFormatedLogTrace(), true) ); } @@ -133,15 +151,23 @@ public function testExportDoesntTrimResult() */ public function testGetFileNameWithAdapterFileName() { - $model = $this->_getMageImportExportModelExportMock(); $basicFileName = 'test_file_name'; - $model->getEntityAdapter()->setFileName($basicFileName); - - $fileName = $model->getFileName(); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + $this->exportAbstractEntityMock->setFileName($basicFileName); + + $fileName = $this->model->getFileName(); $correctDateTime = $this->_getCorrectDateTime($fileName); $this->assertNotNull($correctDateTime); - $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension; + $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $config['file_format']; $this->assertEquals($correctFileName, $fileName); } @@ -150,16 +176,22 @@ public function testGetFileNameWithAdapterFileName() */ public function testGetFileNameWithoutAdapterFileName() { - $model = $this->_getMageImportExportModelExportMock(); - $model->getEntityAdapter()->setFileName(null); - $basicFileName = 'test_entity'; - $model->setEntity($basicFileName); - - $fileName = $model->getFileName(); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + $this->exportAbstractEntityMock->setFileName(null); + + $fileName = $this->model->getFileName(); $correctDateTime = $this->_getCorrectDateTime($fileName); $this->assertNotNull($correctDateTime); - $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension; + $correctFileName = $config['entity'] . '_' . $correctDateTime . '.' . $config['file_format']; $this->assertEquals($correctFileName, $fileName); } diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php index 295b706e3e0b2..129a4cd3a9449 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php @@ -42,7 +42,7 @@ protected function setUp(): void */ public function testConstructorFileDestinationMatch($fileName, $expectedfileName): void { - $this->markTestIncomplete('The implementation of constructor has changed. Rewrite test to cover changes.'); + $this->markTestSkipped('The implementation of constructor has changed. Rewrite test to cover changes.'); $this->directory->method('getRelativePath') ->withConsecutive([$fileName], [$expectedfileName]); diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 3f5b40cef7982..cc78111c53a6c 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -30,6 +30,7 @@ use Magento\ImportExport\Model\Import\Entity\Factory; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use Magento\ImportExport\Model\Source\Upload; use Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase; use Magento\MediaStorage\Model\File\UploaderFactory; @@ -138,6 +139,11 @@ class ImportTest extends AbstractImportTestCase */ private $upload; + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulator; + /** * Set up * @@ -232,6 +238,7 @@ protected function setUp(): void ->method('getDriver') ->willReturn($this->_driver); $this->upload = $this->createMock(Upload::class); + $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); $this->import = $this->getMockBuilder(Import::class) ->setConstructorArgs( [ @@ -252,7 +259,8 @@ protected function setUp(): void [], null, null, - $this->upload + $this->upload, + $this->localeEmulator ] ) ->setMethods( @@ -281,6 +289,17 @@ protected function setUp(): void public function testImportSource() { $entityTypeCode = 'code'; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $this->import->expects($this->any()) + ->method('getData') + ->willReturnMap( + [ + ['locale', null, $locale], + ] + ); $this->_importData->expects($this->any()) ->method('getEntityTypeCode') ->willReturn($entityTypeCode); @@ -333,6 +352,17 @@ public function testImportSourceException() __('URL key for specified store already exists.') ); $entityTypeCode = 'code'; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $this->import->expects($this->any()) + ->method('getData') + ->willReturnMap( + [ + ['locale', null, $locale], + ] + ); $this->_importData->expects($this->any()) ->method('getEntityTypeCode') ->willReturn($entityTypeCode); @@ -363,7 +393,7 @@ public function testImportSourceException() */ public function testGetOperationResultMessages() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -386,7 +416,7 @@ public function testGetAttributeType() */ public function testGetEntity() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -394,7 +424,7 @@ public function testGetEntity() */ public function testGetErrorsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -402,7 +432,7 @@ public function testGetErrorsCount() */ public function testGetErrorsLimit() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -410,7 +440,7 @@ public function testGetErrorsLimit() */ public function testGetInvalidRowsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -418,7 +448,7 @@ public function testGetInvalidRowsCount() */ public function testGetNotices() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -426,7 +456,7 @@ public function testGetNotices() */ public function testGetProcessedEntitiesCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -434,7 +464,7 @@ public function testGetProcessedEntitiesCount() */ public function testGetProcessedRowsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -442,7 +472,7 @@ public function testGetProcessedRowsCount() */ public function testGetWorkingDir() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -450,7 +480,7 @@ public function testGetWorkingDir() */ public function testIsImportAllowed() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -458,7 +488,7 @@ public function testIsImportAllowed() */ public function testUploadSource() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -471,6 +501,10 @@ public function testValidateSource() { $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; $allowedErrorCount = 1; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); $this->errorAggregatorMock->expects($this->once()) ->method('initValidationStrategy') @@ -503,6 +537,7 @@ public function testValidateSource() [ [Import::FIELD_NAME_VALIDATION_STRATEGY, null, $validationStrategy], [Import::FIELD_NAME_ALLOWED_ERROR_COUNT, null, $allowedErrorCount], + ['locale', null, $locale], ] ); @@ -704,7 +739,7 @@ public function unknownEntitiesProvider() */ public function testGetUniqueEntityBehaviors() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php new file mode 100644 index 0000000000000..5e9224713fc03 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Model; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\TranslateInterface; +use Magento\ImportExport\Model\LocaleEmulator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LocaleEmulatorTest extends TestCase +{ + /** + * @var TranslateInterface|MockObject + */ + private $translate; + + /** + * @var RendererInterface|MockObject + */ + private $phraseRenderer; + + /** + * @var ResolverInterface|MockObject + */ + private $localeResolver; + + /** + * @var ResolverInterface|MockObject + */ + private $defaultLocaleResolver; + + /** + * @var LocaleEmulator + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->translate = $this->getMockForAbstractClass(TranslateInterface::class); + $this->phraseRenderer = $this->getMockForAbstractClass(RendererInterface::class); + $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->defaultLocaleResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->model = new LocaleEmulator( + $this->translate, + $this->phraseRenderer, + $this->localeResolver, + $this->defaultLocaleResolver + ); + } + + public function testEmulateWithSpecificLocale(): void + { + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['assertPhraseRenderer']) + ->getMock(); + $mock->expects($this->once()) + ->method('assertPhraseRenderer') + ->willReturnCallback( + fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer()) + ); + $this->defaultLocaleResolver->expects($this->never()) + ->method('getLocale'); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->assertPhraseRenderer(...), $locale); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } + + public function testEmulateWithDefaultLocale(): void + { + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['assertPhraseRenderer']) + ->getMock(); + $mock->expects($this->once()) + ->method('assertPhraseRenderer') + ->willReturnCallback( + fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer()) + ); + $this->defaultLocaleResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->assertPhraseRenderer(...)); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } + + public function testEmulateWithException(): void + { + $exception = new \Exception('Oops! Something went wrong.'); + $this->expectExceptionObject($exception); + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['callbackThatThrowsException']) + ->getMock(); + $mock->expects($this->once()) + ->method('callbackThatThrowsException') + ->willThrowException($exception); + $this->defaultLocaleResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->callbackThatThrowsException(...)); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php new file mode 100644 index 0000000000000..dd13dc6b4c97e --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Model\Source; + +use Laminas\File\Transfer\Adapter\Http; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Math\Random; +use Magento\ImportExport\Helper\Data as DataHelper; +use Magento\ImportExport\Model\Source\Upload; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; +use PHPUnit\Framework\TestCase; + +class UploadTest extends TestCase +{ + /** + * @var Upload + */ + private Upload $upload; + + /** + * @var FileTransferFactory + */ + protected FileTransferFactory $httpFactoryMock; + + /** + * @var DataHelper + */ + private DataHelper $importExportDataMock; + + /** + * @var UploaderFactory + */ + private UploaderFactory $uploaderFactoryMock; + + /** + * @var Random + */ + private Random $randomMock; + + /** + * @var Filesystem + */ + protected Filesystem $filesystemMock; + + /** + * @var Http + */ + private Http $adapterMock; + + /** + * @var Uploader + */ + private Uploader $uploaderMock; + + protected function setUp(): void + { + $directoryAbsolutePath = 'importexport/'; + $this->httpFactoryMock = $this->createPartialMock(FileTransferFactory::class, ['create']); + $this->importExportDataMock = $this->createMock(DataHelper::class); + $this->uploaderFactoryMock = $this->getMockBuilder(UploaderFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->randomMock = $this->getMockBuilder(Random::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->adapterMock = $this->createMock(Http::class); + $directoryWriteMock = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWriteMock->expects($this->once())->method('getAbsolutePath')->willReturn($directoryAbsolutePath); + $this->filesystemMock->expects($this->once())->method('getDirectoryWrite')->willReturn($directoryWriteMock); + $this->upload = new Upload( + $this->httpFactoryMock, + $this->importExportDataMock, + $this->uploaderFactoryMock, + $this->randomMock, + $this->filesystemMock + ); + } + + /** + * @return void + */ + public function testValidateFileUploadReturnsSavedFileArray(): void + { + $allowedExtensions = ['csv', 'zip']; + $savedFileName = 'testString'; + $importFileId = 'import_file'; + $randomStringLength=32; + $this->adapterMock->method('isValid')->willReturn(true); + $this->httpFactoryMock->method('create')->willReturn($this->adapterMock); + $this->uploaderMock = $this->createMock(Uploader::class); + $this->uploaderMock->method('setAllowedExtensions')->with($allowedExtensions); + $this->uploaderMock->method('skipDbProcessing')->with(true); + $this->uploaderFactoryMock->method('create') + ->with(['fileId' => $importFileId]) + ->willReturn($this->uploaderMock); + $this->randomMock->method('getRandomString')->with($randomStringLength); + $this->uploaderMock->method('save')->willReturn(['file' => $savedFileName]); + $result = $this->upload->uploadSource($savedFileName); + $this->assertIsArray($result); + $this->assertEquals($savedFileName, $result['file']); + } +} diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 7b124957d5f57..cb09c448cf0c4 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -28,4 +28,9 @@ <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> </arguments> </type> + <type name="Magento\ImportExport\Model\LocaleEmulator"> + <arguments> + <argument name="defaultLocaleResolver" xsi:type="object">Magento\Backend\Model\Locale\Resolver</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index b4c65aaf5ef11..76c06d382256e 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -13,6 +13,7 @@ <preference for="Magento\ImportExport\Api\Data\ExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\Data\LocalizedExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\ExportManagementInterface" type="Magento\ImportExport\Model\Export\ExportManagement" /> + <preference for="Magento\ImportExport\Model\LocaleEmulatorInterface" type="Magento\ImportExport\Model\LocaleEmulator\Proxy" /> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> <argument name="compositeModules" xsi:type="array"> @@ -39,4 +40,15 @@ </argument> </arguments> </type> + <virtualType name="Magento\ImportExport\Model\DefaultLocaleResolver" type="Magento\Framework\Locale\Resolver"> + <arguments> + <argument name="defaultLocalePath" xsi:type="const">Magento\Directory\Helper\Data::XML_PATH_DEFAULT_LOCALE</argument> + <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> + </arguments> + </virtualType> + <type name="Magento\ImportExport\Model\LocaleEmulator"> + <arguments> + <argument name="defaultLocaleResolver" xsi:type="object">Magento\ImportExport\Model\DefaultLocaleResolver</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php index 51d67e2116a06..0020a0592aa31 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php @@ -7,23 +7,24 @@ namespace Magento\Indexer\Console\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\Console\Cli; +use Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand\ModeInputArgument; use Magento\Indexer\Model\ModeSwitcherInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; /** * Command to set indexer dimensions mode */ class IndexerSetDimensionsModeCommand extends AbstractIndexerCommand { - const INPUT_KEY_MODE = 'mode'; - const INPUT_KEY_INDEXER = 'indexer'; - const DIMENSION_MODE_NONE = 'none'; - const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode'; + public const INPUT_KEY_MODE = 'mode'; + public const INPUT_KEY_INDEXER = 'indexer'; + public const DIMENSION_MODE_NONE = 'none'; + public const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode'; /** * @var string @@ -58,7 +59,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -69,7 +70,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc * @param InputInterface $input * @param OutputInterface $output * @return int @@ -144,17 +145,19 @@ private function getInputList(): array InputArgument::OPTIONAL, $indexerOptionDescription ); - $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL; - foreach ($this->dimensionProviders as $indexer => $provider) { - $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions())); - $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL; - } - $arguments[] = new InputArgument( + $modeOptionDescriptionClosure = function () { + $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL; + foreach ($this->dimensionProviders as $indexer => $provider) { + $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions())); + $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL; + } + return $modeOptionDescription; + }; + $arguments[] = new ModeInputArgument( self::INPUT_KEY_MODE, InputArgument::OPTIONAL, - $modeOptionDescription + $modeOptionDescriptionClosure ); - return $arguments; } diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php new file mode 100644 index 0000000000000..67a2de8dacac2 --- /dev/null +++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand; + +use Symfony\Component\Console\Input\InputArgument; + +/** + * InputArgument that takes callable for description instead of string + */ +class ModeInputArgument extends InputArgument +{ + + /** + * @var callable|null $callableDescription + */ + private $callableDescription; + + /** + * + * @param string $name + * @param int|null $mode + * @param callable|null $callableDescription + * @param string|bool|int|float|array|null $default + */ + public function __construct(string $name, int $mode = null, callable $callableDescription = null, $default = null) + { + $this->callableDescription = $callableDescription; + parent::__construct($name, $mode, '', $default); + } + + /** + * @inheritDoc + */ + public function getDescription() + { + if (null !== $this->callableDescription) { + $description = ($this->callableDescription)(); + $this->callableDescription = null; + return $description; + } + return parent::getDescription(); + } +} diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index ac8b9590e58f4..7be1d5a3a9e21 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -441,8 +441,10 @@ public function reindexAll() } try { $this->getActionInstance()->executeFull(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); + if ($this->workingStateProvider->isWorking($this->getId())) { + $state->setStatus(StateInterface::STATUS_VALID); + $state->save(); + } if (!empty($sharedIndexers)) { $this->resumeSharedViews($sharedIndexers); } diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 78b8fa070b155..7846421daa704 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -59,7 +59,7 @@ public function __construct( IndexerInterfaceFactory $indexerFactory, Indexer\CollectionFactory $indexersFactory, ProcessorInterface $mviewProcessor, - MakeSharedIndexValid $makeSharedValid = null + ?MakeSharedIndexValid $makeSharedValid = null ) { $this->config = $config; $this->indexerFactory = $indexerFactory; @@ -86,9 +86,11 @@ public function reindexAllInvalid() $sharedIndex = $indexerConfig['shared_index'] ?? null; if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - - if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { - $this->sharedIndexesComplete[] = $sharedIndex; + $indexer->load($indexer->getId()); + if ($indexer->isValid()) { + if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { + $this->sharedIndexesComplete[] = $sharedIndex; + } } } } diff --git a/app/code/Magento/Indexer/README.md b/app/code/Magento/Indexer/README.md index ed9ee7ec9723f..0285d3400924a 100644 --- a/app/code/Magento/Indexer/README.md +++ b/app/code/Magento/Indexer/README.md @@ -2,6 +2,7 @@ This module provides Magento Indexing functionality. It allows to: + - read indexers configuration - represent indexers in admin - regenerate indexes by cron schedule @@ -19,6 +20,7 @@ This module is dependent on the following modules: - `Magento_AdminNotification` The Magento_Indexer module creates the following tables in the database: + - `indexer_state` - `mview_state` @@ -45,7 +47,7 @@ The module dispatches the following events: - `clean_cache_by_tags` event in the `\Magento\Indexer\Model\Indexer\CacheCleaner::cleanCache` method. Parameters: - `object` is a `cacheContext` object (`Magento\Framework\Indexer\CacheContext` class) -#### Plugin +#### Plugin - `clean_cache_after_reindex` event in the `\Magento\Indexer\Model\Processor\CleanCache::afterUpdateMview` method. Parameters: - `object` is a `context` object (`Magento\Framework\Indexer\CacheContext` class) @@ -58,6 +60,7 @@ For information about an event in Magento 2, see [Events and observers](https:// ### Layouts This module introduces the following layout handles in the `view/adminhtml/layout` directory: + - `indexer_indexer_list` - `indexer_indexer_list_grid` @@ -75,6 +78,7 @@ There are 2 modes of the Indexers: ### Console commands Magento_Indexers provides console commands: + - `bin/magento indexer:info` - view a list of all indexers - `bin/magento indexer:status [indexer]` - view index status - `bin/magento indexer:reindex [indexer]` - run reindex @@ -87,6 +91,7 @@ Magento_Indexers provides console commands: ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `indexer_reindex_all_invalid` - regenerate indexes for all invalid indexers - `indexer_update_all_views` - update indexer views - `indexer_clean_all_changelogs` - clean indexer view changelogs @@ -94,8 +99,9 @@ Cron group configuration can be set at `etc/crontab.xml`: [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). More information can get at articles: + - [Learn more about indexing](https://developer.adobe.com/commerce/php/development/components/indexing/) -- [Learn more about Indexer optimization](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/indexer-batch.html) +- [Learn more about Indexer optimization](https://developer.adobe.com/commerce/php/development/components/indexing/optimization/) - [Learn more how to add custom indexer](https://developer.adobe.com/commerce/php/development/components/indexing/custom-indexer/) - [Learn how to manage indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html) - [Learn more about Index Management](https://docs.magento.com/user-guide/system/index-management.html) diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml index d92e70cd1993f..75d61fe2261f5 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index bcdfbea78b0b3..451d4c211eadd 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -144,7 +144,8 @@ public function testLoadWithException() public function testGetView() { $indexId = 'indexer_internal_name'; - $this->viewMock->expects($this->once())->method('load')->with('view_test')->willReturnSelf(); + $this->viewMock->expects($this->once()) + ->method('load')->with('view_test')->willReturnSelf(); $this->loadIndexer($indexId); $this->assertEquals($this->viewMock, $this->model->getView()); @@ -224,11 +225,14 @@ public function testReindexAll() $indexId = 'indexer_internal_name'; $this->loadIndexer($indexId); + $this->workingStateProvider->method('isWorking')->willReturnOnConsecutiveCalls(false, true); + $stateMock = $this->createPartialMock( State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -268,7 +272,8 @@ public function testReindexAllWithException() State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -313,7 +318,8 @@ public function testReindexAllWithError() State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -483,7 +489,8 @@ public function testInvalidate() ); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); - $stateMock->expects($this->once())->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf(); $stateMock->expects($this->once())->method('save')->willReturnSelf(); $this->model->invalidate(); } diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index b0a3395519551..ba6216f37f7da 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -102,18 +102,14 @@ public function testReindexAllInvalid(): void $this->configMock->expects($this->once())->method('getIndexers')->willReturn($indexers); $state1Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); - $state1Mock->expects( - $this->once() - )->method( - 'getStatus' - )->willReturn( - StateInterface::STATUS_INVALID - ); + $state1Mock->expects($this->exactly(2)) + ->method('getStatus') + ->willReturnOnConsecutiveCalls(StateInterface::STATUS_INVALID, StateInterface::STATUS_VALID); $indexer1Mock = $this->createPartialMock( Indexer::class, ['load', 'getState', 'reindexAll'] ); - $indexer1Mock->expects($this->once())->method('getState')->willReturn($state1Mock); + $indexer1Mock->expects($this->exactly(2))->method('getState')->willReturn($state1Mock); $indexer1Mock->expects($this->once())->method('reindexAll'); $state2Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); @@ -169,7 +165,10 @@ function ($elem) { $stateMock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); $stateMock->expects($this->any()) ->method('getStatus') - ->willReturn($indexerStates[$indexerData['indexer_id']]); + ->willReturnOnConsecutiveCalls( + $indexerStates[$indexerData['indexer_id']], + StateInterface::STATUS_VALID + ); $indexerMock = $this->createPartialMock(Indexer::class, ['load', 'getState', 'reindexAll']); $indexerMock->expects($this->any())->method('getState')->willReturn($stateMock); $indexerMock->expects($expectedReindexAllCalls[$indexerData['indexer_id']])->method('reindexAll'); diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml index 482ca591811b7..e609f9eace9bb 100644 --- a/app/code/Magento/Indexer/etc/di.xml +++ b/app/code/Magento/Indexer/etc/di.xml @@ -37,6 +37,9 @@ <type name="Magento\Framework\Mview\View\Subscription"> <arguments> <argument name="viewCollection" xsi:type="object" shared="false">Magento\Framework\Mview\View\CollectionInterface</argument> + <argument name="ignoredUpdateColumns" xsi:type="array"> + <item name="updated_at" xsi:type="string">updated_at</item> + </argument> </arguments> </type> <type name="Magento\Indexer\Model\Processor"> diff --git a/app/code/Magento/InstantPurchase/README.md b/app/code/Magento/InstantPurchase/README.md index a4dfa6500e19a..f92335e4c4701 100644 --- a/app/code/Magento/InstantPurchase/README.md +++ b/app/code/Magento/InstantPurchase/README.md @@ -34,13 +34,13 @@ Extension developers can interact with the Magento_InstantPurchase module. For m - `\Magento\InstantPurchase\Model\ShippingMethodChoose\ShippingMethodChooserInterface` - choose shipping method for customer address if available - + - `\Magento\InstantPurchase\Model\InstantPurchaseInterface` - detects instant purchase options for a customer in a store - + - `\Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface` - checks if payment method may be used for instant purchase - + - `\Magento\InstantPurchase\PaymentMethodIntegration\PaymentAdditionalInformationProviderInterface` - provides additional information part specific for payment method diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml index 4248c15b50e05..7cb5b9a12f4d0 100644 --- a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml +++ b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml @@ -19,6 +19,8 @@ <group value="instant_purchase"/> <group value="vault"/> <group value="paypal"/> + <group value="pr_exclude"/> + <group value="3rd_party_integration"/> </annotations> <before> <magentoCLI command="downloadable:domains:add" arguments="example.com static.magento.com" stepKey="addDownloadableDomain"/> diff --git a/app/code/Magento/Integration/README.md b/app/code/Magento/Integration/README.md index 0e17dc80c1355..c9caeb63a9555 100644 --- a/app/code/Magento/Integration/README.md +++ b/app/code/Magento/Integration/README.md @@ -10,11 +10,13 @@ model for request and access token management. The Magento_Integration module is one of the base Magento 2 modules. You cannot disable or uninstall this module. This module is dependent on the following modules: + - `Magento_Store` - `Magento_User` - `Magento_Security` The Magento_Integration module creates the following tables in the database: + - `oauth_consumer` - `oauth_token` - `oauth_nonce` @@ -34,6 +36,7 @@ Extension developers can interact with the Magento_Integration module. For more The module dispatches the following events: #### Model + - `customer_login` event in the `\Magento\Integration\Model\CustomerTokenService::createCustomerAccessToken` method. Parameters: - `customer` is an object (`\Magento\Customer\Api\Data\CustomerInterface` class) @@ -42,6 +45,7 @@ For information about an event in Magento 2, see [Events and observers](https:// ### Layouts This module introduces the following layout handles in the `view/adminhtml/layout` directory: + - `adminhtml_integration_edit` - `adminhtml_integration_grid` - `adminhtml_integration_grid_block` @@ -82,7 +86,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - create a new consumer account - create access token for provided consumer - retrieve access token assigned to the consumer - - load consumer by its ID + - load consumer by its ID - load consumer by its key - execute post to integration (consumer) HTTP Post URL. Generate and return oauth_verifier - delete the consumer data associated with the integration including its token and nonce @@ -95,11 +99,13 @@ For information about a public API in Magento 2, see [Public interfaces & APIs]( ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `outdated_authentication_failures_cleanup` - clearing log of outdated token request authentication failures - `expired_tokens_cleanups` - delete expired customer and admin tokens [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). More information can get at articles: + - [Learn more about an Integration](https://docs.magento.com/user-guide/system/integrations.html) - [Lear how to create an Integration](https://developer.adobe.com/commerce/webapi/get-started/create-integration/) diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml index 6d4e4ed39f6e2..2e51b34b6b3b9 100644 --- a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml @@ -10,7 +10,7 @@ <actionGroup name="AdminFillIntegrationFormActionGroup"> <arguments> <argument name="integration" type="entity" /> - <argument name="currentAdminPassword" type="string" defaultValue="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" /> + <argument name="currentAdminPassword" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" /> </arguments> <fillField selector="{{AdminNewIntegrationFormSection.integrationName}}" userInput="{{integration.name}}" stepKey="fillIntegrationName"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml index d7dca53888f9d..a735a49cabee5 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml @@ -19,6 +19,7 @@ <group value="integration"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml index 0148278ac7aaa..dbb3d005f724f 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-28027"/> <group value="integration"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml index 509521038d4f0..ed1de47c64afb 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml @@ -19,6 +19,7 @@ <group value="mtf_migrated"/> <testCaseId value="MC-14397"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml index a1a9641f6be31..a29c8e5b56e66 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml index 49557be6657bf..d8ad46887e27e 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml @@ -19,6 +19,7 @@ <testCaseId value="MC-14398"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml index c88571ca5ada6..c44396910c146 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml @@ -18,6 +18,7 @@ <group value="integration"/> <testCaseId value="MC-14399"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/LayeredNavigation/README.md b/app/code/Magento/LayeredNavigation/README.md index b27fa3d5360ed..0d324c2a6c2f0 100644 --- a/app/code/Magento/LayeredNavigation/README.md +++ b/app/code/Magento/LayeredNavigation/README.md @@ -17,6 +17,7 @@ Extension developers can interact with the Magento_LayeredNavigation module. For ### Layouts This module introduces the following layout handles in the `view/frontend/layout` directory: + - `catalog_category_view_type_layered` - `catalog_category_view_type_layered_without_children` - `catalogsearch_result_index` @@ -26,6 +27,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `product_attribute_add_form` - `product_attributes_grid` - `product_attributes_listing` @@ -42,7 +44,9 @@ For information about a public API in Magento 2, see [Public interfaces & APIs]( ## Additional information ### Page Layout -This module modifies the following page_layout in the `view/frontend.page_layout` directory: + +This module modifies the following page_layout in the `view/frontend.page_layout` directory: + - `1columns` - moves block `catalog.leftnav` into the `content.top` container - `2columns-left` - moves block `catalog.leftnav` into the `sidebar.main"` container - `2columns-right` - moves block `catalog.leftnav` into the `sidebar.main"` container @@ -50,5 +54,6 @@ This module modifies the following page_layout in the `view/frontend.page_layout - `empty` - moves block `catalog.leftnav` into the `category.product.list.additional` container More information can be found in: + - [Learn more about Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered.html) - [Learn how to Configuring Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered-configuration.html) diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml index 5f74a0c044672..a66662080e221 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml @@ -17,5 +17,6 @@ <element name="actionRemove" type="button" selector="//a[@class='action remove']" /> <element name="nowShoppingByAttribute" type="text" selector="//span[@class='filter-label' and contains(text(),'{{var}}')]" parameterized="true"/> <element name="nowShoppingByAttributeValue" type="text" selector="//span[@class='filter-value' and contains(text(),'{{var}}')]" parameterized="true"/> + <element name="layeredNavigationNthSwatch" type="block" selector="//a[@class='swatch-option-link-layered' and @aria-label='{{attribute_value}}']/div" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml index 80280178e4593..19bbf499906cf 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml @@ -17,6 +17,7 @@ <description value="Admin should be able to uncheck Default Value checkbox for dependent field"/> <severity value="CRITICAL"/> <testCaseId value="MC-12604"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml index 52f5b190c3cb8..6440392164848 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml @@ -22,7 +22,7 @@ <see selector="{{AdminDataGridTableSection.row('1')}}" userInput="{{roleName}}" stepKey="seeUserRole"/> <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="openRoleEditPage"/> <waitForPageLoad stepKey="waitForRoleEditPageLoad"/> - <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> <waitForPageLoad stepKey="waitForRoleResourceTab"/> <selectOption userInput="Custom" selector="{{AdminCreateRoleSection.resourceAccess}}" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml index 13b93f930b005..e3159fbb1e4d0 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml @@ -17,6 +17,7 @@ value="Verify that 'Allow remote shopping assistance' checkbox is present on Edit Account Information page"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml index 7501c71b53f08..a6ba3bc9186df 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml @@ -16,6 +16,7 @@ <description value="Verify Admin can access customer's personal cabinet and change his first and last name using Login as Customer functionality"/> <group value="login_as_customer"/> <severity value="MINOR"></severity> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml index 3a80bbb7a6f2e..555588585d02e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -17,6 +17,7 @@ value="Verify Admin can access customer's personal cabinet and change his default shipping and billing addresses using Login as Customer functionality"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml index 705756bd039d5..13e65d8117ce9 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml @@ -17,6 +17,7 @@ value="Verify that admin user can place order using 'Login as customer' functionality"/> <severity value="BLOCKER"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml index ae99a4dda5593..47afe0b2df29b 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -18,6 +18,7 @@ value="Verify Login as Customer session is ended/invalidated when the related admin session is logged out."/> <severity value="MAJOR"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml index b3297f6bb000d..1d074ba9444f9 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -17,6 +17,7 @@ value="Verify that UI elements are present and links are working if 'Login as customer' functionality enabled"/> <severity value="BLOCKER"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml index 1b31ce1ed5e25..7426f0f5eaa40 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -16,6 +16,7 @@ <description value="Banner is persistent and appears on all pages in session"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml index 6a83e820039d8..f498559322497 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml @@ -17,6 +17,7 @@ value="Verify that Notification Banner is present on page if 'Login as customer' functionality used"/> <severity value="MAJOR"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml index 611bc1044fd00..a372159857f7a 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml @@ -19,6 +19,7 @@ <testCaseId value=""/> <group value="login_as_customer"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomerApi/README.md b/app/code/Magento/LoginAsCustomerApi/README.md index 4f2bf5d82495a..39dc0d7bee6eb 100644 --- a/app/code/Magento/LoginAsCustomerApi/README.md +++ b/app/code/Magento/LoginAsCustomerApi/README.md @@ -6,7 +6,7 @@ This module provides API for ability to login into customer account for an admin - `\Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface`: - contains authentication data - + -`\Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface`: - contains the result of the check whether the login as customer is enabled @@ -26,7 +26,7 @@ This module provides API for ability to login into customer account for an admin - `\Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface`: - get authentication data by secret - + - `\Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface`: - get id of admin logged as customer diff --git a/app/code/Magento/LoginAsCustomerGraphQl/README.md b/app/code/Magento/LoginAsCustomerGraphQl/README.md index 8a42feab75bc2..fa3ff4d8cbcc9 100755 --- a/app/code/Magento/LoginAsCustomerGraphQl/README.md +++ b/app/code/Magento/LoginAsCustomerGraphQl/README.md @@ -19,4 +19,4 @@ This module is a part of Login As Customer feature. [Learn more about Login As Customer feature](https://docs.magento.com/user-guide/customers/login-as-customer.html). -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/LoginAsCustomerLog/README.md b/app/code/Magento/LoginAsCustomerLog/README.md index 6b867473e6b5f..197a5886e07e2 100644 --- a/app/code/Magento/LoginAsCustomerLog/README.md +++ b/app/code/Magento/LoginAsCustomerLog/README.md @@ -11,6 +11,7 @@ For information about a module installation in Magento 2, see [Enable or disable ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `loginascustomer_log_log_index` For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). @@ -18,6 +19,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components You can extend log listing updates using the configuration files located in the directories + - `view/adminhtml/ui_component`: - `login_as_customer_log_listing` diff --git a/app/code/Magento/Marketplace/README.md b/app/code/Magento/Marketplace/README.md index 732d6d77543ae..36ba12e706b4b 100644 --- a/app/code/Magento/Marketplace/README.md +++ b/app/code/Magento/Marketplace/README.md @@ -15,6 +15,7 @@ Extension developers can interact with the Magento_Marketplace module. For more ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `marketplace_index_index` - `marketplace_partners_index` diff --git a/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml b/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml index ac39d72388e6c..fc58db1bed373 100644 --- a/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml +++ b/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml @@ -32,8 +32,8 @@ <h2 class="page-sub-title"><?= $block->escapeHtml(__('Partner search')) ?></h2> <p> <?= $block->escapeHtml(__( - 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver ' . - 'the best possible customer experiences. They are recognized as experts in eCommerce, ' . + 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver ' + . 'the best possible customer experiences. They are recognized as experts in eCommerce, ' . 'search, email marketing, payments, tax, fraud, optimization and analytics, fulfillment, ' . 'and more. Visit the Magento Partner Directory to see all of our trusted partners.' )); ?> @@ -61,7 +61,7 @@ )); ?> </p> <a class="action-secondary" target="_blank" - href="https://marketplace.magento.com/"> + href="https://commercemarketplace.adobe.com/"> <?= $block->escapeHtml(__('Visit Magento Marketplaces')) ?> </a> </div> diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php index c3766484ce4f1..9136f24549289 100644 --- a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php @@ -7,7 +7,6 @@ namespace Magento\MediaContentCatalog\Model\ResourceModel; -use Magento\Catalog\Model\ResourceModel\Product; use Magento\Framework\App\ResourceConnection; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; @@ -23,11 +22,6 @@ class GetEntityContent implements GetEntityContentsInterface */ private $config; - /** - * @var Product - */ - private $productResource; - /** * @var ResourceConnection */ @@ -36,15 +30,12 @@ class GetEntityContent implements GetEntityContentsInterface /** * @param Config $config * @param ResourceConnection $resourceConnection - * @param Product $productResource */ public function __construct( Config $config, - ResourceConnection $resourceConnection, - Product $productResource + ResourceConnection $resourceConnection ) { $this->config = $config; - $this->productResource = $productResource; $this->resourceConnection = $resourceConnection; } diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index e5347f1a11561..622fe7cb2de99 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -19,4 +19,9 @@ <plugin name="synchronize_media_content" type="Magento\MediaContentSynchronization\Plugin\SynchronizeMediaContent"/> </type> + <type name="Magento\MediaContentSynchronization\Console\Command\Synchronize"> + <arguments> + <argument name="synchronizeContent" xsi:type="object">Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaGalleryApi/README.md b/app/code/Magento/MediaGalleryApi/README.md index 8db7d800a9a78..c7a389384e5fe 100644 --- a/app/code/Magento/MediaGalleryApi/README.md +++ b/app/code/Magento/MediaGalleryApi/README.md @@ -34,7 +34,7 @@ Extension developers can interact with the Magento_MediaGalleryApi module. For m - `\Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface`: - get media gallery assets by id attribute - + - `\Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface`: - get media gallery assets by paths in media storage diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md index f53cc0f8f328c..e6a9655d4adba 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/README.md +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -15,6 +15,7 @@ Extension developers can interact with the Magento_MediaGalleryCatalogUi module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `media_gallery_catalog_category_index` For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). @@ -24,9 +25,11 @@ For more information about a layout in Magento 2, see the [Layout documentation] The configuration files located in the directory `view/adminhtml/ui_component`. You can extend media gallery listing updates using the following configuration files: + - `media_gallery_category_listing` This module extends ui components: + - `media_gallery_listing` - `standalone_media_gallery_listing` diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md index a7e8446de77cf..eaa218995ae16 100644 --- a/app/code/Magento/MediaGalleryCmsUi/README.md +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -17,6 +17,7 @@ Extension developers can interact with the Magento_MediaGalleryCmsUi module. For The configuration files located in the directory `view/adminhtml/ui_component`. This module extends ui components: + - `media_gallery_listing` - `standalone_media_gallery_listing` diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml index 7596de07b8922..05d2c30066518 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml @@ -9,7 +9,7 @@ <search> <patterns> <pattern name="media_gallery_renditions">/{{media url=(?:"|&quot;)(?:.renditions)?(.*?)(?:"|&quot;)}}/</pattern> - <pattern name="media_gallery">/{{media url="?(?:.*?\.renditions\/)(.*?)"?}}/</pattern> + <pattern name="media_gallery">/{{media url="?(?:.*?\.renditions\/)?(.*?)"?}}/</pattern> <pattern name="wysiwyg">/src=".*\/media\/(?:.renditions\/)*(.*?)"/</pattern> <pattern name="catalog_image">/^\/?media\/(?:.renditions\/)?(.*)/</pattern> <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php index eebb172e48202..231b7e92f065e 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -12,7 +12,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Driver\File; -use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; @@ -60,14 +60,14 @@ class SynchronizeFiles implements SynchronizeFilesInterface private $importFiles; /** - * @var DateTime + * @var DateTimeFactory */ - private $date; + private $dateFactory; /** * @param File $driver * @param Filesystem $filesystem - * @param DateTime $date + * @param DateTimeFactory $dateFactory * @param LoggerInterface $log * @param GetFileInfo $getFileInfo * @param GetAssetsByPathsInterface $getAssetsByPaths @@ -76,7 +76,7 @@ class SynchronizeFiles implements SynchronizeFilesInterface public function __construct( File $driver, Filesystem $filesystem, - DateTime $date, + DateTimeFactory $dateFactory, LoggerInterface $log, GetFileInfo $getFileInfo, GetAssetsByPathsInterface $getAssetsByPaths, @@ -84,7 +84,7 @@ public function __construct( ) { $this->driver = $driver; $this->filesystem = $filesystem; - $this->date = $date; + $this->dateFactory = $dateFactory; $this->log = $log; $this->getFileInfo = $getFileInfo; $this->getAssetsByPaths = $getAssetsByPaths; @@ -148,7 +148,7 @@ private function getPathsToUpdate(array $paths): array */ private function getFileModificationTime(string $path): string { - return $this->date->gmtDate( + return $this->dateFactory->create()->gmtDate( self::DATE_FORMAT, $this->getFileInfo->execute($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() ); diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml index 82bd1303eda74..9f088dbf2915a 100644 --- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -50,4 +50,9 @@ <type name="Magento\Framework\App\Config\Value"> <plugin name="admin_system_config_adobe_stock_save_plugin" type="Magento\MediaGallerySynchronization\Plugin\MediaGallerySyncTrigger"/> </type> + <type name="Magento\MediaGallerySynchronization\Console\Command\Synchronize"> + <arguments> + <argument name="synchronizeAssets" xsi:type="object">Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md index 584f242ccd425..c1dc448bc7990 100644 --- a/app/code/Magento/MediaGalleryUi/README.md +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -17,6 +17,7 @@ Extension developers can interact with the Magento_MediaGalleryUi module. For mo ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `media_gallery_index_index` - `media_gallery_media_index` @@ -32,6 +33,7 @@ You can extend media gallery listing updates using the following configuration f - `standalone_media_gallery_listing` This module extends ui components: + - `cms_block_listing` - `cms_page_listing` - `product_listing` diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md index f4b0d1a1d6dce..585428276f13e 100644 --- a/app/code/Magento/MediaGalleryUiApi/README.md +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -11,4 +11,3 @@ For information about module installation in Magento 2, see [Enable or disable m For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). [Learn more about New Media Gallery](https://docs.magento.com/user-guide/cms/media-gallery.html). - diff --git a/app/code/Magento/MediaStorage/README.md b/app/code/Magento/MediaStorage/README.md index f77ddac816cb4..3e401c7aa6058 100644 --- a/app/code/Magento/MediaStorage/README.md +++ b/app/code/Magento/MediaStorage/README.md @@ -36,5 +36,6 @@ Extension developers can interact with the Magento_MediaStorage module. For more [Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html). More information can get at articles: + - [Learn how to configure Media Storage Database](https://docs.magento.com/user-guide/system/media-storage-database.html). - [Learn how to Resize catalog images](https://developer.adobe.com/commerce/frontend-core/guide/themes/configure/#resize-catalog-images) diff --git a/app/code/Magento/MediaStorage/etc/di.xml b/app/code/Magento/MediaStorage/etc/di.xml index 5cdcbb3b2b9a9..db03601835fd7 100644 --- a/app/code/Magento/MediaStorage/etc/di.xml +++ b/app/code/Magento/MediaStorage/etc/di.xml @@ -28,6 +28,8 @@ </type> <type name="Magento\MediaStorage\Console\Command\ImagesResizeCommand"> <arguments> + <argument name="appState" xsi:type="object">Magento\Framework\App\State\Proxy</argument> + <argument name="imageResize" xsi:type="object">Magento\MediaStorage\Service\ImageResize\Proxy</argument> <argument name="imageResizeScheduler" xsi:type="object">Magento\MediaStorage\Service\ImageResizeScheduler\Proxy</argument> </arguments> </type> diff --git a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php index c097f461e621b..49540e248319b 100644 --- a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php +++ b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php @@ -7,6 +7,7 @@ namespace Magento\MessageQueue\Model; +use Magento\Framework\MessageQueue\CountableQueueInterface; use Magento\Framework\MessageQueue\QueueRepository; /** @@ -40,6 +41,9 @@ public function __construct(QueueRepository $queueRepository) public function execute($connectionName, $queueName) { $queue = $this->queueRepository->get($connectionName, $queueName); + if ($queue instanceof CountableQueueInterface) { + return $queue->count() > 0; + } $message = $queue->dequeue(); if ($message) { $queue->reject($message); diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php new file mode 100644 index 0000000000000..2ea95960ae8e0 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Test\Unit\Model; + +use Magento\Framework\MessageQueue\CountableQueueInterface; +use Magento\Framework\MessageQueue\EnvelopeInterface; +use Magento\Framework\MessageQueue\QueueInterface; +use Magento\Framework\MessageQueue\QueueRepository; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for CheckIsAvailableMessagesInQueue + */ +class CheckIsAvailableMessagesInQueueTest extends TestCase +{ + /** + * @var QueueRepository|MockObject + */ + private $queueRepository; + + /** + * @var CheckIsAvailableMessagesInQueue + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->queueRepository = $this->createMock(QueueRepository::class); + $this->model = new CheckIsAvailableMessagesInQueue( + $this->queueRepository + ); + } + + public function testExecuteNotCountableAndNotEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(QueueInterface::class); + $message = $this->getMockForAbstractClass(EnvelopeInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('dequeue') + ->willReturn($message); + $queue->expects($this->once()) + ->method('reject') + ->willReturn($message); + $this->assertTrue($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteNotCountableAndEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(QueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('dequeue') + ->willReturn(null); + $this->assertFalse($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteCountableAndNotEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(CountableQueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('count') + ->willReturn(1); + $queue->expects($this->never()) + ->method('dequeue'); + $this->assertTrue($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteCountableAndEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(CountableQueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('count') + ->willReturn(0); + $queue->expects($this->never()) + ->method('dequeue'); + $this->assertFalse($this->model->execute($connectionName, $queueName)); + } +} diff --git a/app/code/Magento/Msrp/README.md b/app/code/Magento/Msrp/README.md index deef2e0dcef5e..a82a5b391c5df 100644 --- a/app/code/Magento/Msrp/README.md +++ b/app/code/Magento/Msrp/README.md @@ -1,9 +1,10 @@ # Magento_Msrp module -The **Magento_Msrp** module is responsible for Manufacturer’s Suggested Retail Price functionality. +The **Magento_Msrp** module is responsible for Manufacturer's Suggested Retail Price functionality. A current module provides base functional for msrp pricing rendering, configuration and calculation. ## Installation + The Magento_Msrp module creates the following attributes: Entity type - `catalog_product`. @@ -14,7 +15,8 @@ Attribute group - `Advanced Pricing`. - `msrp_display_actual_price_type` -Display Actual Price **Pay attention** if described attributes not removed when the module is removed/disabled, it would trigger errors -because they use models and blocks from Magento_Msrp module: +because they use models and blocks from Magento_Msrp module: + - `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type` - `\Magento\Msrp\Model\Product\Attribute\Source\Type\Price` - `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type\Price` @@ -22,36 +24,39 @@ because they use models and blocks from Magento_Msrp module: For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure + `Pricing\` - directory contains interfaces and implementation for msrp pricing calculations - (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers + (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers and price models. - + `Pricing\Price\` - the directory contains declares msrp price model interfaces and implementations. `Pricing\Renderer\` - contains price renderers implementations. For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). - + ## Extensibility - - Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`. - + + Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`. + For example: - ``` - <type name="Magento\Msrp\Pricing\MsrpPriceCalculator"> - <arguments> - <argument name="msrpPriceCalculators" xsi:type="array"> - <item name="configurable" xsi:type="array"> - <item name="productType" xsi:type="const">Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE</item> - <item name="priceCalculator" xsi:type="object">Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator</item> - </item> - </argument> - </arguments> - </type> -``` + + ```xml +<type name="Magento\Msrp\Pricing\MsrpPriceCalculator"> + <arguments> + <argument name="msrpPriceCalculators" xsi:type="array"> + <item name="configurable" xsi:type="array"> + <item name="productType" xsi:type="const">Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE</item> + <item name="priceCalculator" xsi:type="object">Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator</item> + </item> + </argument> + </arguments> +</type> +``` + More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/). - + Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). [The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module. @@ -62,13 +67,15 @@ This module observes the following event: `etc/frontend/` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. `etc/webapi_rest` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. `etc/webapi_soap` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). @@ -105,7 +112,7 @@ This module introduces the following layouts and layout handles: ### UI components -Module provides product admin form modifier: +Module provides product admin form modifier: `Magento\Msrp\Ui\DataProvider\Product\Form\Modifier\Msrp` - removes `msrp_display_actual_price_type` field from the form if config disabled else adds `validate-zero-or-greater` validation to the fild. @@ -114,10 +121,13 @@ Module provides product admin form modifier: ### Catalog attributes A current module extends `etc/catalog_attributes.xml` and provides following attributes for `quote_item` group: + - `msrp` - `msrp_display_actual_price_type` ### Extension Attributes + The Magento_Msrp provides extension attributes for `Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface` + - attribute code: `msrp` - attribute type: `Magento\Msrp\Api\Data\ProductRender\MsrpPriceInfoInterface` diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml index 874edf0dff9e3..941ede7c3538d 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml @@ -11,6 +11,7 @@ <test name="AdminCheckProductListPriceAttributesTest"> <annotations> <group value="msrp"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleOutOfStockProductWithSpecialPriceCostAndMsrp" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml index 86732ba1e18bf..2bb27f8ac9487 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-40419"/> <useCaseId value="MC-35640"/> <group value="msrp"/> + <group value="cloud"/> </annotations> <before> <!-- Enable Minimum advertised Price --> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml index 42bf5772e96e0..a12c3c69e058e 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-12292"/> <useCaseId value="MC-10973"/> <group value="Msrp"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/MsrpConfigurableProduct/README.md b/app/code/Magento/MsrpConfigurableProduct/README.md index 7afb0d834693c..de3160ad7c51c 100644 --- a/app/code/Magento/MsrpConfigurableProduct/README.md +++ b/app/code/Magento/MsrpConfigurableProduct/README.md @@ -9,8 +9,8 @@ For information about a module installation in Magento 2, see [Enable or disable ## Structure -`Pricing\` - directory contains implementation of msrp price calculation -for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class). +`Pricing\` - directory contains implementation of msrp price calculation +for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class). For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). diff --git a/app/code/Magento/MsrpGroupedProduct/README.md b/app/code/Magento/MsrpGroupedProduct/README.md index 1c2a5c15a146d..605ca4714a0bb 100644 --- a/app/code/Magento/MsrpGroupedProduct/README.md +++ b/app/code/Magento/MsrpGroupedProduct/README.md @@ -9,8 +9,8 @@ For information about a module installation in Magento 2, see [Enable or disable ## Structure -`Pricing\` - directory contains implementation of msrp price calculation -for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class). +`Pricing\` - directory contains implementation of msrp price calculation +for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class). For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). @@ -33,7 +33,7 @@ For information about a UI component in Magento 2, see [Overview of UI component ### collection attributes -Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection` +Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection` in `Magento\MsrpGroupedProduct\Plugin\Model\Product\Type\Grouped` plugin. For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Multishipping/README.md b/app/code/Magento/Multishipping/README.md index 436357afa0436..2e1c88dc1818b 100644 --- a/app/code/Magento/Multishipping/README.md +++ b/app/code/Magento/Multishipping/README.md @@ -11,26 +11,27 @@ For information about a module installation in Magento 2, see [Enable or disable For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). - - ## Extensibility + +## Extensibility Developers can interact with the module and change behaviour using type configuration feature. -Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes. -As result, we will get changed behaviour, new logic or something what our business need. +Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes. +As result, we will get changed behaviour, new logic or something what our business need. For example: -``` + +```xml <type name="Magento\Multishipping\Model\Checkout\Type\Multishipping"> <arguments> <argument name="paymentSpecification" xsi:type="object">multishippingPaymentSpecification</argument> </arguments> </type> ``` + Yo can check this configuration and find more examples in the `etc/frontend/di.xml` file. - -More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/). +More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/). Extension developers can interact with the Magento_Multishipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). @@ -42,7 +43,7 @@ This module observes the following event: `etc/frontend/` - - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file. + - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file. The module dispatches the following events: @@ -78,7 +79,7 @@ The module interacts with the following layout handles: `view/frontend/layout` directory: - `checkout_cart_index` - + This module introduces the following layouts and layout handles: `view/frontend/layout` directory: @@ -135,5 +136,4 @@ Module introduces the new pages: More information about [layout types](https://developer.adobe.com/commerce/frontend-core/guide/layouts/types/). - For information about significant changes in patch releases, see [2.3.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml new file mode 100644 index 0000000000000..a31fc9a0e02fb --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.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="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" extends="AssertStorefrontMultishippingAddressAndItemActionGroup"> + <annotations> + <description>Verify item information on Ship to Multiple Addresses page for UK and Germany.</description> + </annotations> + <arguments> + <argument name="addressQtySequenceNumber" type="string" defaultValue="1"/> + </arguments> + <remove keyForRemoval="verifyAddress"/> + <seeInField selector="{{MultishippingSection.shippingAddressSelector(addressQtySequenceNumber)}}" userInput="{{firstName}} {{lastName}}, {{addressStreetLine1}}, {{city}}, {{postCode}}, {{country}}" stepKey="verifyAddressDetails"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml new file mode 100644 index 0000000000000..012648deae5e9 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup"> + <annotations> + <description>Goes to the provided Storefront URL. Selects the provided Product Option under the Product Attribute. Fills in the provided Quantity. Clicks Add to Cart. Validates that the Success Message is present.</description> + </annotations> + <arguments> + <argument name="urlKey" type="string"/> + <argument name="color" type="string"/> + <argument name="qty" type="string"/> + </arguments> + + <amOnPage url="{{urlKey}}.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productOptionSelectByColor}}" stepKey="waitForOptions"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelectByColor}}" userInput="{{color}}" stepKey="selectOption1"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{qty}}" stepKey="fillProductQuantity"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartDisabled}}" stepKey="waitForAddToCartButtonToRemoveDisabledState"/> + <waitForElementClickable selector="{{StorefrontProductActionSection.addToCart}}" stepKey="waitForAddToCartButton"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml new file mode 100644 index 0000000000000..c401098a07943 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup" extends="StorefrontAssertBillingAddressInBillingInfoStepActionGroup"> + <annotations> + <description>Assert that Billing Address block contains provided Address data for Germany.</description> + </annotations> + <remove keyForRemoval="seeState"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml index 304f0a9c7a12a..5273a56bb9edf 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml @@ -18,5 +18,8 @@ <element name="productLink" type="button" selector="(//form[@id='checkout_multishipping_form']//a[contains(text(),'{{productName}}')])[{{sequenceNumber}}]" parameterized="true"/> <element name="removeItemButton" type="button" selector="//a[contains(@title, 'Remove Item')][{{var}}]" parameterized="true"/> <element name="back" type="button" selector=".action.back"/> + <element name="addressSection" type="text" selector="//div[@class='block-title']/strong[text()='Address {{var}} ']" parameterized="true"/> + <element name="flatRateCharge" type="text" selector="//span[@class='price' and text()='${{price}}']/../../label[contains(text(),'Fixed')]" parameterized="true"/> + <element name="enterNewAddress" type="button" selector=".action.add"/> </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 ef41ed3f47f3a..af62a0fc28337 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml @@ -11,6 +11,8 @@ <section name="ShippingMethodSection"> <element name="shippingMethodRadioButton" type="select" selector="//input[@class='radio']"/> <element name="selectShippingMethod" type="radio" selector="//div[@class='block block-shipping'][position()={{shippingBlockPosition}}]//dd[position()={{shippingMethodPosition}}]//input[@class='radio']" parameterized="true" timeout="5"/> + <element name="shippingMethod" type="radio" selector="//div[@class='block block-shipping'][position()={{shippingBlockPosition}}]//dd[position()={{shippingMethodPosition}}]" parameterized="true" timeout="5"/> <element name="goToBillingInfo" type="button" selector=".action.primary.continue"/> + <element name="productDetails" type="text" selector="//a[text()='{{var1}}']/../../..//td[@class='col qty' and text()='{{var2}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml index cf6bd10b0e8df..279967c2a3be0 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml @@ -17,6 +17,7 @@ <element name="checkmoneyorderonOverViewPage" type="text" selector="//dt[contains(text() , 'Check / Money order')]"/> <element name="othershippingitems" type="text" selector="//div[@class='block block-other']//div/strong[contains(text(),'Other items in your order')]/../..//div[2]//td/strong/a[contains(text(),'{{var}}')]" parameterized="true" /> <element name="shippingaddresstext" type="text" selector="//div[@class='box box-order-shipping-address']//span[contains(text(),'Shipping Address')]" /> + <element name="grandTotalAmount" type="text" selector="//div[@id='checkout-review-submit']/div[@class='grand totals']"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml index 6cfc09c1653fd..c2ae07b987976 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml @@ -10,5 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontMultishippingCheckoutBillingToolbarSection"> <element name="goToReviewOrder" type="button" selector="button.action.primary.continue"/> + <element name="changeBillingAddress" type="button" selector="//span[text()='Change']"/> + <element name="selectBillingAddress" type="button" selector="//a[text()='333-33-333-33']/../../..//span[text()='Select Address']"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml index 39ad54fc66710..3972821a9871f 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26572"/> <useCaseId value="MC-26572"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml new file mode 100644 index 0000000000000..cb09866438a79 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml @@ -0,0 +1,395 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="MultishipmentCheckoutWithDifferentProductTest"> + <annotations> + <features value="Multishipment"/> + <stories value="Multishipping checkout with different product's types"/> + <title value="Multishipping checkout with different product's types"/> + <description value="Multishipping checkout with different product's types"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4267"/> + <group value="Multishipment"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="Customer_US_UK_DE" stepKey="createCustomer"/> + <!-- Create category and 2 simple product --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="firstSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="secondSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">15</field> + </createData> + <!-- Create group product with created above simple products --> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="firstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="secondSimpleProduct"/> + </updateData> + <!--edit default quantity of each simple product under grouped product.--> + <amOnPage url="{{AdminProductEditPage.url($$createGroupedProduct.id$$)}}" stepKey="openGroupedProductEditPage"/> + <actionGroup ref="FillDefaultQuantityForLinkedToGroupProductInGridActionGroup" stepKey="fillDefaultQtyForVirtualProduct"> + <argument name="productName" value="$$firstSimpleProduct.name$$"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="FillDefaultQuantityForLinkedToGroupProductInGridActionGroup" stepKey="fillDefaultQtyForSecondProduct"> + <argument name="productName" value="$$secondSimpleProduct.name$$"/> + <argument name="qty" value="2"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAndCloseCreatedGroupedProduct"/> + <!-- Create Configurable Product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Search for the Created Configurable Product --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="openConfigurableProductForEdit"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> + <!--Update the Created Configurable Product --> + <actionGroup ref="AdminCreateThreeConfigurationsForConfigurableProductActionGroup" stepKey="editConfigurableProduct"> + <argument name="product" value="{{createConfigurableProduct}}"/> + <argument name="redColor" value="{{colorProductAttribute2.name}}"/> + <argument name="blueColor" value="{{colorProductAttribute3.name}}"/> + <argument name="whiteColor" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + <!--Create bundle product with dynamic price with two simple products --> + <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="firstSimpleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="secondSimpleProduct"/> + </createData> + <!--Assign bundle product to category--> + <amOnPage url="{{AdminProductEditPage.url($$createDynamicBundleProduct.id$$)}}" stepKey="openBundleProductEditPage"/> + <actionGroup ref="AdminAssignCategoryToProductAndSaveActionGroup" stepKey="assignCategoryToProduct"> + <argument name="categoryName" value="$createCategory.name$"/> + </actionGroup> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteAllProducts"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete the Created Color attribute--> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteWhiteColorAttribute"> + <argument name="Color" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteRedColorAttribute"> + <argument name="Color" value="{{colorProductAttribute2.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteBlueColorAttribute"> + <argument name="Color" value="{{colorProductAttribute3.name}}"/> + </actionGroup> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addFirstSimpleProductToCart"> + <argument name="product" value="$$firstSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSecondSimpleProductToCart"> + <argument name="product" value="$$secondSimpleProduct$$"/> + </actionGroup> + <!-- Add grouped product to shopping cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addGroupedProductToCart"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <!-- goto Bundle Product Page--> + <amOnPage url="{{StorefrontProductPage.url($createDynamicBundleProduct.custom_attributes[url_key]$)}}" stepKey="navigateToBundleProduct"/> + <!-- Add Bundle first Product to Cart --> + <actionGroup ref="StorefrontAddBundleProductToTheCartActionGroup" stepKey="addFirstBundleProductToCart"> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Add Bundle second Product to Cart --> + <actionGroup ref="StorefrontAddBundleProductToTheCartActionGroup" stepKey="addSecondBundleProductToCart"> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!--Add different configurable product to cart.--> + <actionGroup ref="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup" stepKey="addRedConfigurableProductToCart"> + <argument name="urlKey" value="$createConfigurableProduct.custom_attributes[url_key]$" /> + <argument name="color" value="Red"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup" stepKey="addBlueConfigurableProductToCart"> + <argument name="urlKey" value="$createConfigurableProduct.custom_attributes[url_key]$" /> + <argument name="color" value="Blue"/> + <argument name="qty" value="3"/> + </actionGroup> + <!--verify total product quantity in minicart.--> + <seeElement selector="{{StorefrontMinicartSection.quantity(11)}}" stepKey="seeAddedProductQuantityInMiniCart"/> + <!-- Go to Shopping Cart page --> + <actionGroup ref="clickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <!-- Link "Check Out with Multiple Addresses" is shown --> + <seeLink userInput="Check Out with Multiple Addresses" stepKey="seeLinkIsPresent"/> + <!-- Click Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <!-- Check Ship to Multiple Address Page is opened--> + <waitForPageLoad stepKey="waitForAddressPage"/> + <seeInCurrentUrl url="{{MultishippingCheckoutAddressesPage.url}}" stepKey="seeShipToMultipleAddressesPageIsOpened"/> + <!--select different address To Ship for different products--> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddressFromThreeOption"> + <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="selectSecondAddressFromThreeOption"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectThirdAddressFromThreeOption"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFourthAddressFromThreeOption"> + <argument name="sequenceNumber" value="4"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFifthAddressFromThreeOption"> + <argument name="sequenceNumber" value="5"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSixthAddressFromThreeOption"> + <argument name="sequenceNumber" value="6"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSeventhAddressFromThreeOption"> + <argument name="sequenceNumber" value="7"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectEighthAddressFromThreeOption"> + <argument name="sequenceNumber" value="8"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectNinthAddressFromThreeOption"> + <argument name="sequenceNumber" value="9"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectTenthAddressFromThreeOption"> + <argument name="sequenceNumber" value="10"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectEleventhAddressFromThree"> + <argument name="sequenceNumber" value="11"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <click selector="{{SingleShippingSection.updateAddress}}" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad time="30" stepKey="waitForShippingInformationAfterUpdated"/> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemActionGroup" stepKey="verifyFirstLineAllDetails"> + <argument name="sequenceNumber" value="1"/> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="{{US_Address_NY.firstname}}"/> + <argument name="lastName" value="{{US_Address_NY.lastname}}"/> + <argument name="city" value="{{US_Address_NY.city}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postCode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="addressStreetLine1" value="{{US_Address_NY.street[0]}}"/> + <argument name="addressStreetLine2" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemActionGroup" stepKey="verifySecondLineQtyAllDetails"> + <argument name="sequenceNumber" value="2"/> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="{{US_Address_NY.firstname}}"/> + <argument name="lastName" value="{{US_Address_NY.lastname}}"/> + <argument name="city" value="{{US_Address_NY.city}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postCode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="addressStreetLine1" value="{{US_Address_NY.street[0]}}"/> + <argument name="addressStreetLine2" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyThirdLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="3"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyFourthLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="4"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyFifthLineAllDetails"> + <argument name="productSequenceNumber" value="3"/> + <argument name="addressQtySequenceNumber" value="5"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifySixthLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="6"/> + <argument name="productName" value="$createDynamicBundleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifySeventhLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="7"/> + <argument name="productName" value="$createDynamicBundleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyEighthLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="8"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyNinthLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="9"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyTenthLineAllDetails"> + <argument name="productSequenceNumber" value="3"/> + <argument name="addressQtySequenceNumber" value="10"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyEleventhLineAllDetails"> + <argument name="productSequenceNumber" value="4"/> + <argument name="addressQtySequenceNumber" value="11"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="{{DE_Address_Berlin_Not_Default_Address.city}}"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <!--verify multishipment all three section--> + <seeElement selector="{{MultishippingSection.addressSection('1')}}" stepKey="firstAddressSection"/> + <seeElement selector="{{MultishippingSection.addressSection('2')}}" stepKey="secondAddressSection"/> + <seeElement selector="{{MultishippingSection.addressSection('3')}}" stepKey="thirdAddressSection"/> + <!--verify flat rate charge for all three section--> + <seeElement selector="{{MultishippingSection.flatRateCharge('10.00')}}" stepKey="verifyFirstFlatRateAmount"/> + <seeElement selector="{{MultishippingSection.flatRateCharge('15.00')}}" stepKey="verifySecondFlatRateAmount"/> + <seeElement selector="{{MultishippingSection.flatRateCharge('30.00')}}" stepKey="verifyThirdFlatRateAmount"/> + <!-- Click On Continue to Billing--> + <click selector="{{StorefrontMultishippingCheckoutShippingToolbarSection.continueToBilling}}" stepKey="clickContinueToBilling"/> + <waitForPageLoad stepKey="waitForCheckoutShippingToolbarPageLoad"/> + <!-- See Billing Information Page is opened--> + <seeInCurrentUrl url="{{MultishippingCheckoutBillingPage.url}}" stepKey="seeBillingPageIsOpened"/> + <!-- click on change billing address button --> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.changeBillingAddress}}" stepKey="clickChangeBillingAddressButton"/> + <!-- select new billing address--> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.selectBillingAddress}}" stepKey="selectBillingAddress"/> + <wait stepKey="waitForPaymentPageToLoad" time="10"/> + <!-- Page contains Payment Method --> + <seeElement selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" stepKey="CheckMoney"/> + <!-- Select Payment method "Check / Money Order --> + <conditionalClick selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" dependentSelector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <!-- Select Payment method e.g. "Check / Money Order" and click Go to Review Your Order --> + <waitForElement selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="waitForElementgoToReviewOrder"/> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="clickGoToReviewOrder"/> + <!-- See Order review Page is opened--> + <seeInCurrentUrl url="{{MultishippingCheckoutOverviewPage.url}}" stepKey="seeMultishipingCheckoutOverviewPageIsOpened"/> + <!-- Check Page contains customer's billing address on OverViewPage--> + <actionGroup ref="StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup" stepKey="assertCustomerBillingInformationOverViewPage"> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + </actionGroup> + <!-- Check Payment Method on OverViewPage--> + <seeElement selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorderonOverViewPage}}" stepKey="seeCheckMoneyorderonOverViewPage"/> + <!--Check total amount --> + <see selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.grandTotalAmount}}" userInput="Grand Total: $215.00" stepKey="seeGrandTotalAmount"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <!--Check Thank you for your purchase!" page is opened --> + <see selector="{{StorefrontMultipleShippingMethodSection.successMessage}}" userInput="Successfully ordered" stepKey="seeSuccessMessage"/> + <!--Grab Order ID of placed all 3 order --> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('3')}}" stepKey="grabThirdOrderId"/> + <!-- Go to My Account > My Orders and verify orderId--> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabThirdOrderId})}}" stepKey="seeThirdOrder"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml index e22df0a8f3063..001b86c841a65 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-18519"/> <group value="Multishipment"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml index 084e0ffc9f3ac..457a145e2c083 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-18519"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml index 82563e5055c2a..07b2834fa04d4 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml @@ -44,6 +44,7 @@ </actionGroup> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> <click selector="{{MultishippingSection.checkoutWithMultipleAddresses}}" stepKey="proceedMultishipping"/> + <waitForElementClickable selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="waitForCreateAccount"/> <click selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="clickCreateAccount"/> <seeElement selector="{{CheckoutShippingSection.region}}" stepKey="seeRegionSelector"/> </test> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml index f05c6e355bb1d..3c40bc4f6f286 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-18519"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml index 8108de8f9e2de..260ff06c3c5b7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml @@ -15,6 +15,7 @@ <description value="Verify Multishipping checkout flow if cart contains virtual product type"/> <testCaseId value="MC-26600"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create default category --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml index 815d406c68bfa..339459f66f2be 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-21738"/> <group value="Multishipment"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" before="goToProduct1" stepKey="createSubtotalCartPriceRuleActionsSection"> <argument name="ruleName" value="CartPriceRuleConditionForSubtotalForMultiShipping"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml index 1d9b6e99a1ea7..5d9dd1999835f 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42067"/> <useCaseId value="MC-41924"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml index 8c0df3c70677d..9d4ddd35c217a 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-38994"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml index 8205ab962b9fe..9d9d00223c725 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17461"/> <useCaseId value="MAGETWO-99490"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml index 632950120474d..582a9265fd4ba 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-36921"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml index 02ef596cda7c9..d9dc06342e3d3 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-39007"/> <useCaseId value="MC-38825"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -64,6 +65,7 @@ <actionGroup ref="StorefrontGoCheckoutWithMultipleAddressesActionGroup" stepKey="goCheckoutWithMultipleAddresses"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goBackToShoppingCartPage"/> + <actionGroup ref="StorefrontCheckoutCartFillEstimateShippingAndTaxActionGroup" stepKey="updateShippingAndTaxEstimator" /> <actionGroup ref="AssertStorefrontCheckoutPaymentSummaryTotalActionGroup" stepKey="assertSummaryTotal"> <argument name="orderTotal" value="{$grabTotal}"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml index 79d2a6942e6de..00334e182b753 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-41697"/> <useCaseId value="MC-40021"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml index 4377b8cfd8c18..e5a514fcb46e2 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17871"/> <useCaseId value="MC-17469"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php index cbc2e951782f2..6d29fc8aee576 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Queue.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Queue.php @@ -5,16 +5,18 @@ */ namespace Magento\MysqlMq\Model\Driver; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\MessageQueue\CountableQueueInterface; use Magento\Framework\MessageQueue\EnvelopeInterface; -use Magento\Framework\MessageQueue\QueueInterface; use Magento\MysqlMq\Model\QueueManagement; use Magento\Framework\MessageQueue\EnvelopeFactory; +use Magento\MysqlMq\Model\ResourceModel\Queue as QueueResourceModel; use Psr\Log\LoggerInterface; /** * Queue based on MessageQueue protocol */ -class Queue implements QueueInterface +class Queue implements CountableQueueInterface { /** * @var QueueManagement @@ -46,6 +48,11 @@ class Queue implements QueueInterface */ private $logger; + /** + * @var QueueResourceModel + */ + private $queueResourceModel; + /** * Queue constructor. * @@ -55,6 +62,7 @@ class Queue implements QueueInterface * @param string $queueName * @param int $interval * @param int $maxNumberOfTrials + * @param QueueResourceModel|null $queueResourceModel */ public function __construct( QueueManagement $queueManagement, @@ -62,7 +70,8 @@ public function __construct( LoggerInterface $logger, $queueName, $interval = 5, - $maxNumberOfTrials = 3 + $maxNumberOfTrials = 3, + ?QueueResourceModel $queueResourceModel = null ) { $this->queueManagement = $queueManagement; $this->envelopeFactory = $envelopeFactory; @@ -70,6 +79,8 @@ public function __construct( $this->interval = $interval; $this->maxNumberOfTrials = $maxNumberOfTrials; $this->logger = $logger; + $this->queueResourceModel = $queueResourceModel + ?? ObjectManager::getInstance()->get(QueueResourceModel::class); } /** @@ -151,4 +162,12 @@ public function push(EnvelopeInterface $envelope) [$this->queueName] ); } + + /** + * @inheritDoc + */ + public function count(): int + { + return $this->queueResourceModel->getMessagesCount($this->queueName); + } } diff --git a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php index 2a45eafc63f24..a110f1efdd0c5 100644 --- a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php +++ b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php @@ -5,6 +5,8 @@ */ namespace Magento\MysqlMq\Model\ResourceModel; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\Expression; use Magento\MysqlMq\Model\QueueManagement; /** @@ -240,6 +242,35 @@ public function changeStatus($relationIds, $status) ); } + /** + * Get number of pending messages in the queue + * + * @param string $queueName + * @return int + */ + public function getMessagesCount(string $queueName): int + { + $connection = $this->getConnection(); + $select = $connection->select() + ->from( + ['queue_message' => $this->getMessageTable()], + )->join( + ['queue_message_status' => $this->getMessageStatusTable()], + 'queue_message.id = queue_message_status.message_id' + )->join( + ['queue' => $this->getQueueTable()], + 'queue.id = queue_message_status.queue_id' + )->where( + 'queue_message_status.status IN (?)', + [QueueManagement::MESSAGE_STATUS_NEW, QueueManagement::MESSAGE_STATUS_RETRY_REQUIRED] + )->where('queue.name = ?', $queueName); + + $select->reset(Select::COLUMNS); + $select->columns(new Expression('COUNT(*)')); + + return (int) $connection->fetchOne($select); + } + /** * Get name of table storing message statuses and associations to queues. * diff --git a/app/code/Magento/MysqlMq/README.md b/app/code/Magento/MysqlMq/README.md index be9c23dda9bda..9da1e54fd787e 100644 --- a/app/code/Magento/MysqlMq/README.md +++ b/app/code/Magento/MysqlMq/README.md @@ -2,7 +2,7 @@ **Magento_MysqlMq** provides message queue implementation based on MySQL. -Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring` +Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring` class. This script is executed by Magento post each schema installation or upgrade stage and populates the queue table. @@ -14,7 +14,6 @@ Module creates the following tables: - `queue_message` - Queue messages - `queue_message_status` - Relation table to keep associations between queues and messages - For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information diff --git a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php index 2b9013d594709..d1013c454a17b 100644 --- a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php +++ b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php @@ -9,6 +9,7 @@ use Laminas\Http\Request; use Magento\Framework\HTTP\LaminasClient; use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Serialize\SerializerInterface; use Magento\NewRelicReporting\Model\Config; use Psr\Log\LoggerInterface; @@ -37,21 +38,29 @@ class Deployments */ protected $clientFactory; + /** + * @var SerializerInterface + */ + private $serializer; + /** * Constructor * * @param Config $config * @param LoggerInterface $logger * @param LaminasClientFactory $clientFactory + * @param SerializerInterface $serializer */ public function __construct( Config $config, LoggerInterface $logger, - LaminasClientFactory $clientFactory + LaminasClientFactory $clientFactory, + SerializerInterface $serializer ) { $this->config = $config; $this->logger = $logger; $this->clientFactory = $clientFactory; + $this->serializer = $serializer; } /** @@ -97,8 +106,7 @@ public function setDeployment($description, $change = false, $user = false, $rev 'revision' => $revision ] ]; - - $client->setParameterPost($params); + $client->setRawBody($this->serializer->serialize($params)); try { $response = $client->send(); diff --git a/app/code/Magento/NewRelicReporting/README.md b/app/code/Magento/NewRelicReporting/README.md index 97bceff86da3e..a2cebb0ee45ff 100644 --- a/app/code/Magento/NewRelicReporting/README.md +++ b/app/code/Magento/NewRelicReporting/README.md @@ -1,10 +1,11 @@ # Magento_NewRelicReporting module -This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making. +This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making. ## Installation Before installing this module, note that the Magento_NewRelicReporting is dependent on the following modules: + - `Magento_Store` - `Magento_Customer` - `Magento_Backend` @@ -13,6 +14,7 @@ Before installing this module, note that the Magento_NewRelicReporting is depend - `Magento_Config` This module creates the following tables in the database: + - `reporting_counts` - `reporting_module_status` - `reporting_orders` @@ -34,6 +36,7 @@ Extension developers can interact with the Magento_NewRelicReporting module. For ### Console commands The Magento_NewRelicReporting provides console commands: + - `bin/magento newrelic:create:deploy-marker <message> <change_log> [<user>]` - check the deploy queue for entries and create an appropriate deploy marker [Learn more about command's parameters](https://experienceleague.adobe.com/docs/commerce-operations/reference/magento-open-source.html#newreliccreatedeploy-marker). @@ -41,6 +44,7 @@ The Magento_NewRelicReporting provides console commands: ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `magento_newrelicreporting_cron` - runs collecting all new relic reports [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml index 3be9d2d8445de..a202dcf23a291 100644 --- a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml +++ b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="NewRelicReporting"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php index ae632441f1074..d64458fe8e763 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php @@ -12,6 +12,7 @@ use Laminas\Http\Response; use Magento\Framework\HTTP\LaminasClient; use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Serialize\SerializerInterface; use Magento\NewRelicReporting\Model\Apm\Deployments; use Magento\NewRelicReporting\Model\Config; use PHPUnit\Framework\MockObject\MockObject; @@ -45,31 +46,24 @@ class DeploymentsTest extends TestCase */ protected $loggerMock; + /** + * @var SerializerInterface|MockObject + */ + private $serializerMock; + protected function setUp(): void { - $this->httpClientFactoryMock = $this->getMockBuilder(LaminasClientFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - - $this->httpClientMock = $this->getMockBuilder(LaminasClient::class) - ->setMethods(['send', 'setUri', 'setMethod', 'setHeaders', 'setParameterPost']) - ->disableOriginalConstructor() - ->getMock(); - - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->configMock = $this->getMockBuilder(Config::class) - ->setMethods(['getNewRelicApiUrl', 'getNewRelicApiKey', 'getNewRelicAppId']) - ->disableOriginalConstructor() - ->getMock(); + $this->httpClientFactoryMock = $this->createMock(LaminasClientFactory::class); + $this->httpClientMock = $this->createMock(LaminasClient::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->configMock = $this->createMock(Config::class); + $this->serializerMock = $this->createMock(SerializerInterface::class); $this->model = new Deployments( $this->configMock, $this->loggerMock, - $this->httpClientFactoryMock + $this->httpClientFactoryMock, + $this->serializerMock ); } @@ -97,9 +91,13 @@ public function testSetDeploymentRequestOk() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) @@ -163,9 +161,13 @@ public function testSetDeploymentBadStatus() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) @@ -225,9 +227,13 @@ public function testSetDeploymentRequestFail() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml index cd8b0f46087a4..0fdce7f722e03 100644 --- a/app/code/Magento/NewRelicReporting/etc/di.xml +++ b/app/code/Magento/NewRelicReporting/etc/di.xml @@ -53,4 +53,9 @@ </argument> </arguments> </type> + <type name="Magento\NewRelicReporting\Model\Apm\Deployments"> + <arguments> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index f4e72c61953f0..bd7c62e82e630 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -30,8 +30,6 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_isStoreFilter = false; /** - * Date - * * @var \Magento\Framework\Stdlib\DateTime\DateTime */ protected $_date; diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php index 64f6c066f01b6..d59bf28310778 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php @@ -32,7 +32,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_storeTable; /** - * Queue joined flag + * Flag for joined queue * * @var boolean */ @@ -82,8 +82,7 @@ public function __construct( } /** - * Constructor - * Configures collection + * Constructor configures collection * * @return void */ diff --git a/app/code/Magento/Newsletter/README.md b/app/code/Magento/Newsletter/README.md index b9aac71e222b2..b51cc7508d3fb 100644 --- a/app/code/Magento/Newsletter/README.md +++ b/app/code/Magento/Newsletter/README.md @@ -5,15 +5,18 @@ This module allows clients to subscribe for information about new promotions and ## Installation Before installing this module, note that the Magento_Newsletter is dependent on the following modules: + - `Magento_Store` - `Magento_Customer` - `Magento_Eav` - `Magento_Widget` Before disabling or uninstalling this module, note that the following modules depends on this module: + - `Magento_NewsletterGraphQl` This module creates the following tables in the database: + - `newsletter_subscriber` - `newsletter_template` - `newsletter_queue` @@ -34,6 +37,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `newsletter_problem_block` - `newsletter_problem_grid` @@ -53,7 +57,7 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `newsletter_template_preview` - `newsletter_template_preview_popup` - `preview` - + - `view/frontend/layout`: - `customer_account` - `customer_account_create` @@ -65,6 +69,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components This module extends customer form ui component the configuration file located in the `view/base/ui_component` directory: + - `customer_form` For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). @@ -76,7 +81,7 @@ For information about a UI component in Magento 2, see [Overview of UI component ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `newsletter_send_all` - schedules newsletter sending [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). - diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml index 44104f3adf0d9..e2954fcbb6f97 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml @@ -12,6 +12,7 @@ <arguments> <argument name="email" type="string"/> </arguments> + <waitForElementVisible selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" stepKey="waitForElementEmailVisible"></waitForElementVisible> <fillField stepKey="fillEmailField" selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" userInput="{{email}}"/> <click selector="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> </actionGroup> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index be8849ce99391..de5e0bbedcbb4 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84377"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index b12629a666afc..ddd6b8ab16945 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to add widget to WYSIWYG Editor Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MC-6070"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml index c472d262a34c8..5e6c618debcf9 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able delete newsletter subscribers"/> <severity value="CRITICAL"/> <group value="newsletter"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml index d6bee2c618849..34c68efb05a0b 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml index 0db48811e8026..f02e0f61a3da1 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml index 93429bfd14f85..012270cf1c63f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml index 5e35f5aab60cd..ec07415e4a768 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml @@ -16,6 +16,7 @@ <title value="Empty name for Guest Customer"/> <description value="'Customer First Name' and 'Customer Last Name' should be empty for Guest Customer in Newsletter Subscribers Grid"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml index 746c786ef3dac..7e9c52f183a2f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index 1b56f12049973..54fd53d09bc6b 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -17,6 +17,7 @@ <description value="Newsletter subscription when user is registered on 2 stores"/> <severity value="MAJOR"/> <testCaseId value="MC-25840"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml index a3b2a5f93a12d..cdbab53b65784 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml @@ -16,6 +16,7 @@ <description value="Admin should see TinyMCE is the native WYSIWYG on Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84683"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/OfflinePayments/README.md b/app/code/Magento/OfflinePayments/README.md index 1e9c3fb5426fb..8b34b3f2c4999 100644 --- a/app/code/Magento/OfflinePayments/README.md +++ b/app/code/Magento/OfflinePayments/README.md @@ -1,7 +1,8 @@ # Magento_OfflinePayments module -This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods). +This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods). These methods are the following: + - Bank transfer - Cash on delivery - Check / Money Order @@ -10,6 +11,7 @@ These methods are the following: ## Installation Before installing this module, note that the Magento_OfflinePayments is dependent on the following modules: + - `Magento_Store` - `Magento_Catalog` @@ -26,6 +28,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_index_index` - `multishipping_checkout_billing` diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml new file mode 100644 index 0000000000000..f25b23f8b9d9b --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.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="StorefrontSelectCheckMoneyOrderActionGroup"> + <annotations> + <description>Select "Check / Money Order payment method if radio button available otherwise continue."</description> + </annotations> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/README.md b/app/code/Magento/OfflineShipping/README.md index c45767d3190da..440f2bc3e1541 100644 --- a/app/code/Magento/OfflineShipping/README.md +++ b/app/code/Magento/OfflineShipping/README.md @@ -1,7 +1,8 @@ # Magento_OfflineShipping module -This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods. +This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods. Namely, the following: + - Free Shipping - Flat Rate - Table Rates @@ -10,6 +11,7 @@ Namely, the following: ## Installation Before installing this module, note that the Magento_OfflineShipping is dependent on the following modules: + - `Magento_Store` - `Magento_Sales` - `Magento_Quote` @@ -19,6 +21,7 @@ Before installing this module, note that the Magento_OfflineShipping is dependen The Magento_OfflineShipping module creates the `shipping_tablerate` table in the database. This module modifies the following tables in the database: + - `salesrule` - adds column `simple_free_shipping` - `sales_order_item` - adds column `free_shipping` - `quote_address` - adds column `free_shipping` @@ -38,6 +41,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_cart_index` - `checkout_index_index` @@ -46,6 +50,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `sales_rule_form` - `salesrulestaging_update_form` @@ -54,6 +59,7 @@ For information about a UI component in Magento 2, see [Overview of UI component ## Additional information You can get more information about offline shipping methods in magento at the articles: + - [How to configure Free Shipping](https://docs.magento.com/user-guide/shipping/shipping-free.html) - [How to configure Flat Rate](https://docs.magento.com/user-guide/shipping/shipping-flat-rate.html) - [How to configure Table Rates](https://docs.magento.com/user-guide/shipping/shipping-table-rate.html) diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml index d225e5fa28f97..fb3b950cd2afa 100644 --- a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -15,6 +15,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-38271"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <!-- Add simple product --> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml new file mode 100644 index 0000000000000..ae55756990c3a --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.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="StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest"> + <annotations> + <features value="Shipping"/> + <stories value="Offline Shipping Methods"/> + <title value="Free Shipping Should Not Applicable if Other Discount Reduce the Matching Amount"/> + <description value="Free Shipping Should Not Applicable if Other Discount Reduce the Matching Amount"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-7886"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Create cart price rule --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create active cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup" stepKey="createFreeShippingCartPriceRule"> + <argument name="ruleName" value="CartPriceRuleFreeShippingAppliedOnly"/> + </actionGroup> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax"/> + <argument name="couponCode" value="CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code"/> + </actionGroup> + <!-- Add simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">100.00</field> + </createData> + </before> + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteFreeShippingCartPriceRule"> + <argument name="ruleName" value="{{CartPriceRuleFreeShippingAppliedOnly.name}}"/> + </actionGroup> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.name}}"/> + </actionGroup> +<!-- Remove simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Assert that table rate value is correct for US --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$0.00" stepKey="seeFlatShippingZero"/> + + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyButton"/> + <actionGroup ref="StorefrontShoppingCartFillCouponCodeFieldActionGroup" stepKey="fillDiscountCodeField"> + <argument name="discountCode" value="{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code}}"/> + </actionGroup> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyDiscountButton"/> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value='You used coupon code "{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code}}".'/> + </actionGroup> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxFormAfterCouponApplied"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodFormAfterCouponApplied"/> + <!-- Sometimes the shipping loading masks are not done loading --> + <waitForPageLoad stepKey="waitForShippingLoaded"/> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$5.00" stepKey="seeFlatShippingPrice"/> + </test> +</tests> diff --git a/app/code/Magento/OpenSearch/README.md b/app/code/Magento/OpenSearch/README.md index 36ab12072d572..eeeffcd968ef3 100644 --- a/app/code/Magento/OpenSearch/README.md +++ b/app/code/Magento/OpenSearch/README.md @@ -1,3 +1,3 @@ -#Magento_OpenSearch module +# Magento_OpenSearch module Magento_OpenSearch module allows using OpenSearch 1.x engine for the product searching capabilities. diff --git a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml index 7f694a1168f6c..c725bcaf69a40 100644 --- a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml +++ b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-6631"/> <group value="catalog_search"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set catalog/search/engine opensearch" stepKey="setSearchEngine"/> diff --git a/app/code/Magento/PageCache/Controller/Block.php b/app/code/Magento/PageCache/Controller/Block.php index e69614496c66d..b32866524d9d9 100644 --- a/app/code/Magento/PageCache/Controller/Block.php +++ b/app/code/Magento/PageCache/Controller/Block.php @@ -1,6 +1,5 @@ <?php /** - * PageCache controller * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -9,6 +8,8 @@ use Magento\Framework\Serialize\Serializer\Base64Json; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Validator\RegexFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; abstract class Block extends \Magento\Framework\App\Action\Action @@ -40,28 +41,42 @@ abstract class Block extends \Magento\Framework\App\Action\Action */ private $layoutCacheKeyName = 'mage_pagecache'; + /** + * @var RegexFactory + */ + private RegexFactory $regexValidatorFactory; + + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Translate\InlineInterface $translateInline * @param Json $jsonSerializer * @param Base64Json $base64jsonSerializer * @param LayoutCacheKeyInterface $layoutCacheKey + * @param RegexFactory|null $regexValidatorFactory */ public function __construct( \Magento\Framework\App\Action\Context $context, \Magento\Framework\Translate\InlineInterface $translateInline, Json $jsonSerializer = null, Base64Json $base64jsonSerializer = null, - LayoutCacheKeyInterface $layoutCacheKey = null + LayoutCacheKeyInterface $layoutCacheKey = null, + ?RegexFactory $regexValidatorFactory = null ) { parent::__construct($context); $this->translateInline = $translateInline; $this->jsonSerializer = $jsonSerializer - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); + ?: ObjectManager::getInstance()->get(Json::class); $this->base64jsonSerializer = $base64jsonSerializer - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Base64Json::class); + ?: ObjectManager::getInstance()->get(Base64Json::class); $this->layoutCacheKey = $layoutCacheKey - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); + ?: ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); + $this->regexValidatorFactory = $regexValidatorFactory + ?: ObjectManager::getInstance()->get(RegexFactory::class); } /** @@ -79,6 +94,9 @@ protected function _getBlocks() } $blocks = $this->jsonSerializer->unserialize($blocks); $handles = $this->base64jsonSerializer->unserialize($handles); + if (!$this->validateHandleParam($handles)) { + return []; + } $layout = $this->_view->getLayout(); $this->layoutCacheKey->addCacheKeys($this->layoutCacheKeyName); @@ -95,4 +113,22 @@ protected function _getBlocks() return $data; } + + /** + * Validates handles parameter + * + * @param array $handles + * @return bool + */ + private function validateHandleParam($handles): bool + { + $validator = $this->regexValidatorFactory->create(['pattern' => self::VALIDATION_RULE_PATTERN]); + foreach ($handles as $handle) { + if ($handle && !$validator->isValid($handle)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php index 5340f5204e21e..061cc801d5d1e 100644 --- a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php +++ b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php @@ -5,6 +5,7 @@ */ namespace Magento\PageCache\Model\App\FrontController; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\Response\Http as ResponseHttp; /** @@ -73,7 +74,7 @@ public function aroundDispatch( $result = $this->kernel->load(); if ($result === false) { $result = $proceed($request); - if ($result instanceof ResponseHttp) { + if ($result instanceof ResponseHttp && !$result instanceof NotCacheableInterface) { $this->addDebugHeaders($result); $this->kernel->process($result); } diff --git a/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php new file mode 100644 index 0000000000000..26b8715c36447 --- /dev/null +++ b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model\App\Request\Http; + +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\PageCache\IdentifierInterface; + +/** + * Page unique identifier + */ +class IdentifierForSave implements IdentifierInterface +{ + /** + * @param Http $request + * @param Context $context + * @param Json $serializer + */ + public function __construct( + private Http $request, + private Context $context, + private Json $serializer + ) { + } + + /** + * Return unique page identifier + * + * @return string + */ + public function getValue() + { + $data = [ + $this->request->isSecure(), + $this->request->getUriString(), + $this->context->getVaryString() + ]; + + return sha1($this->serializer->serialize($data)); + } +} diff --git a/app/code/Magento/PageCache/README.md b/app/code/Magento/PageCache/README.md index 1b109926fd9f0..30e46cb560d55 100644 --- a/app/code/Magento/PageCache/README.md +++ b/app/code/Magento/PageCache/README.md @@ -1,4 +1,4 @@ The PageCache module provides functionality of caching full pages content in Magento application. An administrator may switch between built-in caching and Varnish caching. Built-in caching is default and ready to use without the need of any external tools. Requests and responses are managed by PageCache plugin. It loads data from cache and returns a response. If data is not present in cache, it passes the request to Magento and waits for the response. Response is then saved in cache. Blocks can be set as private blocks by setting the property '_isScopePrivate' to true. These blocks contain personalized information and are not cached in the server. These blocks are being rendered using AJAX call after the page is loaded. Contents are cached in browser instead. -Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example `<block class="Block\Class" name="blockname" cacheable="false" />`. Pages containing such blocks are not cached. \ No newline at end of file +Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example `<block class="Block\Class" name="blockname" cacheable="false" />`. Pages containing such blocks are not cached. diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index eeac1c2fe1124..5851c8dbac5c9 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="pagecache"/> <group value="cookie"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml index a7cf367ff3030..4d1a174632661 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -31,6 +31,7 @@ <waitForPageLoad stepKey="waitForPageCacheManagementLoad"/> <!-- Check 'Flush Static Files Cache' not visible in production mode. --> - <dontSee selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton" /> + <scrollTo selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Catalog Images Cache')}}" stepKey="scrollToAdditionalCacheButtons"/> + <dontSeeElement selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton"/> </test> </tests> diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php index c0c4eac7d5255..a003d6aa3bd1f 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php @@ -18,6 +18,8 @@ use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Layout; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\PageCache\Controller\Block; use Magento\PageCache\Controller\Block\Esi; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; @@ -64,6 +66,11 @@ class EsiTest extends TestCase */ protected $translateInline; + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * Set up before test */ @@ -98,6 +105,16 @@ protected function setUp(): void $this->translateInline = $this->getMockForAbstractClass(InlineInterface::class); + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex(self::VALIDATION_RULE_PATTERN); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + $helperObjectManager = new ObjectManager($this); $this->action = $helperObjectManager->getObject( Esi::class, @@ -106,7 +123,8 @@ protected function setUp(): void 'translateInline' => $this->translateInline, 'jsonSerializer' => new Json(), 'base64jsonSerializer' => new Base64Json(), - 'layoutCacheKey' => $this->layoutCacheKeyMock + 'layoutCacheKey' => $this->layoutCacheKeyMock, + 'regexValidatorFactory' => $regexFactoryMock ] ); } diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php index 89e4b06994a71..7cec177a3a0bb 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php @@ -18,6 +18,8 @@ use Magento\Framework\View\Layout; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; use Magento\Framework\View\Layout\ProcessorInterface; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\PageCache\Controller\Block; use Magento\PageCache\Controller\Block\Render; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; @@ -69,6 +71,11 @@ class RenderTest extends TestCase */ protected $layoutCacheKeyMock; + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * @inheritDoc */ @@ -111,6 +118,16 @@ protected function setUp(): void $this->translateInline = $this->getMockForAbstractClass(InlineInterface::class); + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex(self::VALIDATION_RULE_PATTERN); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + $helperObjectManager = new ObjectManager($this); $this->action = $helperObjectManager->getObject( Render::class, @@ -119,7 +136,8 @@ protected function setUp(): void 'translateInline' => $this->translateInline, 'jsonSerializer' => new Json(), 'base64jsonSerializer' => new Base64Json(), - 'layoutCacheKey' => $this->layoutCacheKeyMock + 'layoutCacheKey' => $this->layoutCacheKeyMock, + 'regexValidatorFactory' => $regexFactoryMock ] ); } diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php index 0827f84a21192..30e0e6a0276ad 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php @@ -11,6 +11,7 @@ use Laminas\Http\Header\GenericHeader; use Magento\Framework\App\FrontControllerInterface; use Magento\Framework\App\PageCache\Kernel; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\PageCache\Version; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http; @@ -243,6 +244,41 @@ public function testAroundDispatchDisabled($state): void ); } + /** + * @return void + */ + public function testAroundNotCacheableResponse(): void + { + $this->configMock + ->expects($this->once()) + ->method('getType') + ->willReturn(Config::BUILT_IN); + $this->configMock->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + $this->versionMock + ->expects($this->once()) + ->method('process'); + $this->kernelMock->expects($this->once()) + ->method('load') + ->willReturn(false); + $this->stateMock->expects($this->never()) + ->method('getMode'); + $this->kernelMock->expects($this->never()) + ->method('process'); + $this->responseMock->expects($this->never()) + ->method('setHeader'); + $notCacheableResponse = $this->createMock(NotCacheableInterface::class); + $this->assertSame( + $notCacheableResponse, + $this->plugin->aroundDispatch( + $this->frontControllerMock, + fn () => $notCacheableResponse, + $this->requestMock + ) + ); + } + /** * @return array */ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php new file mode 100644 index 0000000000000..4a9b884e6c5cb --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Model\App\Request\Http; + +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\PageCache\Model\App\Request\Http\IdentifierForSave; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class IdentifierForSaveTest extends TestCase +{ + /** + * Test value for cache vary string + */ + private const VARY = '123'; + + /** + * @var Context|MockObject + */ + private mixed $contextMock; + + /** + * @var HttpRequest|MockObject + */ + private mixed $requestMock; + + /** + * @var IdentifierForSave + */ + private IdentifierForSave $model; + + /** + * @var Json|MockObject + */ + private mixed $serializerMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->requestMock = $this->getMockBuilder(HttpRequest::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->serializerMock = $this->getMockBuilder(Json::class) + ->onlyMethods(['serialize']) + ->disableOriginalConstructor() + ->getMock(); + $this->serializerMock->expects($this->any()) + ->method('serialize') + ->willReturnCallback( + function ($value) { + return json_encode($value); + } + ); + + $this->model = new IdentifierForSave( + $this->requestMock, + $this->contextMock, + $this->serializerMock + ); + parent::setUp(); + } + + /** + * Test get identifier for save value. + * + * @return void + */ + public function testGetValue(): void + { + $this->requestMock->expects($this->any()) + ->method('isSecure') + ->willReturn(true); + + $this->requestMock->expects($this->any()) + ->method('getUriString') + ->willReturn('http://example.com/path1/'); + + $this->contextMock->expects($this->any()) + ->method('getVaryString') + ->willReturn(self::VARY); + + $this->assertEquals( + sha1( + json_encode( + [ + true, + 'http://example.com/path1/', + self::VARY + ] + ) + ), + $this->model->getValue() + ); + } +} diff --git a/app/code/Magento/PageCache/etc/frontend/di.xml b/app/code/Magento/PageCache/etc/frontend/di.xml index 1aaa331da7025..7f4d05ae206bf 100644 --- a/app/code/Magento/PageCache/etc/frontend/di.xml +++ b/app/code/Magento/PageCache/etc/frontend/di.xml @@ -26,4 +26,9 @@ <type name="Magento\Framework\App\Response\Http"> <plugin name="response-http-page-cache" type="Magento\PageCache\Model\App\Response\HttpPlugin"/> </type> + <type name="Magento\Framework\App\PageCache\Kernel"> + <arguments> + <argument name="identifierForSave" xsi:type="object">Magento\PageCache\Model\App\Request\Http\IdentifierForSave</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index eb0c2194848ea..7622025cc296e 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -180,6 +180,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 6acd160b183aa..335ffe289e721 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -179,6 +179,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index e1b2e3184613b..ee89dc8d22d7e 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -183,6 +183,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index b62e86e0f831c..f52fb081cd7dd 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -67,7 +67,7 @@ public function getPostParams(): array $params = []; foreach ($this->_request->getPostValue() as $name => $value) { if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { - $value = utf8_encode($value); + $value = mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); } $params[$name] = $value; } diff --git a/app/code/Magento/Paypal/Model/PayLaterConfig.php b/app/code/Magento/Paypal/Model/PayLaterConfig.php index 438ec0f0235d8..c638e7427971b 100644 --- a/app/code/Magento/Paypal/Model/PayLaterConfig.php +++ b/app/code/Magento/Paypal/Model/PayLaterConfig.php @@ -15,17 +15,17 @@ class PayLaterConfig /** * Configuration key for Styles settings */ - const CONFIG_KEY_STYLE = 'style'; + public const CONFIG_KEY_STYLE = 'style'; /** * Configuration key for Position setting */ - const CONFIG_KEY_POSITION = 'position'; + public const CONFIG_KEY_POSITION = 'position'; /** * Checkout payment step placement */ - const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment'; + public const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment'; /** * @var Config @@ -91,11 +91,11 @@ public function getSectionConfig(string $section, string $key) { if (!array_key_exists($section, $this->configData)) { $sectionName = $section === self::CHECKOUT_PAYMENT_PLACEMENT - ? self::CHECKOUT_PAYMENT_PLACEMENT : "${section}page"; + ? self::CHECKOUT_PAYMENT_PLACEMENT : "{$section}page"; $this->configData[$section] = [ - 'display' => (boolean)$this->config->getPayLaterConfigValue("${sectionName}_display"), - 'position' => $this->config->getPayLaterConfigValue("${sectionName}_position"), + 'display' => (boolean)$this->config->getPayLaterConfigValue("{$sectionName}_display"), + 'position' => $this->config->getPayLaterConfigValue("{$sectionName}_position"), 'style' => $this->getConfigStyles($sectionName) ]; } @@ -113,17 +113,17 @@ private function getConfigStyles(string $sectionName): array { $logoType = $logoPosition = $textColor = $textSize = null; $color = $ratio = null; - $styleLayout = $this->config->getPayLaterConfigValue("${sectionName}_stylelayout"); + $styleLayout = $this->config->getPayLaterConfigValue("{$sectionName}_stylelayout"); if ($styleLayout === 'text') { - $logoType = $this->config->getPayLaterConfigValue("${sectionName}_logotype"); + $logoType = $this->config->getPayLaterConfigValue("{$sectionName}_logotype"); if ($logoType === 'primary' || $logoType === 'alternative') { - $logoPosition = $this->config->getPayLaterConfigValue("${sectionName}_logoposition"); + $logoPosition = $this->config->getPayLaterConfigValue("{$sectionName}_logoposition"); } - $textColor = $this->config->getPayLaterConfigValue("${sectionName}_textcolor"); - $textSize = $this->config->getPayLaterConfigValue("${sectionName}_textsize"); + $textColor = $this->config->getPayLaterConfigValue("{$sectionName}_textcolor"); + $textSize = $this->config->getPayLaterConfigValue("{$sectionName}_textsize"); } elseif ($styleLayout === 'flex') { - $color = $this->config->getPayLaterConfigValue("${sectionName}_color"); - $ratio = $this->config->getPayLaterConfigValue("${sectionName}_ratio"); + $color = $this->config->getPayLaterConfigValue("{$sectionName}_color"); + $ratio = $this->config->getPayLaterConfigValue("{$sectionName}_ratio"); } return [ diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php index 705b667ab2f65..53c4a4e083181 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php @@ -9,41 +9,38 @@ use Magento\Paypal\Model\Payflow\Service\Response\ValidatorInterface; use Magento\Paypal\Model\Payflow\Transparent; -/** - * Class CVV2Match - */ class CVV2Match implements ValidatorInterface { /** * Result of the card security code (CVV2) check */ - const CVV2MATCH = 'cvv2match'; + public const CVV2MATCH = 'cvv2match'; /** * This field returns the transaction amount, or if performing a partial authorization, * the amount approved for the partial authorization. */ - const AMT = 'amt'; + public const AMT = 'amt'; /** * Message if validation fail */ - const ERROR_MESSAGE = 'Card security code does not match.'; + public const ERROR_MESSAGE = 'Card security code does not match.'; /**#@+ Values of the response */ - const RESPONSE_YES = 'y'; + public const RESPONSE_YES = 'y'; - const RESPONSE_NO = 'n'; + public const RESPONSE_NO = 'n'; - const RESPONSE_NOT_SUPPORTED = 'x'; + public const RESPONSE_NOT_SUPPORTED = 'x'; /**#@-*/ /**#@+ Validation settings payments */ - const CONFIG_ON = 1; + public const CONFIG_ON = 1; - const CONFIG_OFF = 0; + public const CONFIG_OFF = 0; - const CONFIG_NAME = 'avs_security_code'; + public const CONFIG_NAME = 'avs_security_code'; /**#@-*/ /** @@ -55,7 +52,7 @@ class CVV2Match implements ValidatorInterface */ public function validate(DataObject $response, Transparent $transparentModel) { - if ($transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) { + if ((int)$transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) { return true; } diff --git a/app/code/Magento/Paypal/README.md b/app/code/Magento/Paypal/README.md index 0ed4f2e90291b..555449257de5c 100644 --- a/app/code/Magento/Paypal/README.md +++ b/app/code/Magento/Paypal/README.md @@ -1,4 +1,5 @@ Module Magento\PayPal implements integration with the PayPal payment system. Namely, it enables the following payment methods: + * PayPal Express Checkout * PayPal Payments Standard * PayPal Payments Pro diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml new file mode 100644 index 0000000000000..c97e580a2c93a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminPayPalPayflowProWithValutActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal Payflow pro credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalPaymentsProConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad"/> + <click selector ="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" stepKey="expandOtherPaypalConfigButton"/> + <scrollTo selector="{{PayPalPayflowProConfigSection.paymentGateway(countryCode)}}" stepKey="scrollToConfigure"/> + <click selector ="{{PayPalPayflowProConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalPaymentsProConfigureBtn"/> + <scrollTo selector="{{PayPalPayflowProConfigSection.partner(countryCode)}}" stepKey="scrollToBottom"/> + <fillField selector ="{{PayPalPayflowProConfigSection.partner(countryCode)}}" userInput="{{credentials.paypal_paymentspro_parner}}" stepKey="inputPartner"/> + <fillField selector ="{{PayPalPayflowProConfigSection.user(countryCode)}}" userInput="{{credentials.paypal_paymentspro_user}}" stepKey="inputUser"/> + <fillField selector ="{{PayPalPayflowProConfigSection.vendor(countryCode)}}" userInput="{{credentials.paypal_paymentspro_vendor}}" stepKey="inputVendor"/> + <fillField selector ="{{PayPalPayflowProConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_paymentspro_password}}" stepKey="inputPassword"/> + <selectOption selector="{{PayPalPayflowProConfigSection.testmode(countryCode)}}" userInput="Yes" stepKey="enableTestMode"/> + <selectOption selector ="{{PayPalPayflowProConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <selectOption selector ="{{PayPalPayflowProConfigSection.enableVault(countryCode)}}" userInput="Yes" stepKey="enableSolutionValut"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml index b653858f770e9..e20b38638cadb 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml @@ -23,8 +23,10 @@ <click selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="clickWPSExpressConfigureBtn"/> <waitForElementVisible selector="{{payPalConfigType.enableSolution(countryCode)}}" stepKey="waitForWPSExpressEnable"/> <selectOption selector="{{payPalConfigType.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableWPSExpressSolution"/> + <wait time="2" stepKey="waitForPopupToAppear" /> <seeInPopup userInput="There is already another PayPal solution enabled. Enable this solution instead?" stepKey="seeAlertMessage"/> <acceptPopup stepKey="acceptEnablePopUp"/> + <waitForElementClickable selector="{{AdminConfigSection.saveButton}}" stepKey="waitForSaveConfigClickable" /> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml index a2c7b7d82a349..0a1077e0c18eb 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml @@ -19,11 +19,11 @@ <conditionalClick selector="{{PayPalPaymentSection.existingAccountLoginBtn}}" dependentSelector="{{PayPalPaymentSection.existingAccountLoginBtn}}" visible="true" stepKey="skipAccountCreationAndLogin"/> <waitForPageLoad stepKey="waitForLoginPageLoad"/> <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> - <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/PAYPAL_LOGIN}}" stepKey="fillEmail"/> <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> <waitForElementVisible selector="{{PayPalPaymentSection.password}}" stepKey="waitForPasswordField"/> <click selector="{{PayPalPaymentSection.password}}" stepKey="focusOnPasswordField"/> - <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/PAYPAL_PWD}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index 95e69cf6e93cf..4e88bbe73e2e6 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -214,7 +214,7 @@ </entity> <entity name="VisaDefaultCardInfo"> <data key="cardNumberEnding">1111</data> - <data key="cardExpire">01/2030</data> + <data key="cardExpire">1/2030</data> </entity> <entity name="SamplePaypalExpressConfig2" type="paypal_express_config"> <data key="paypal_express_email">rlus_1349181941_biz@ebay.com</data> @@ -223,4 +223,10 @@ <data key="paypal_express_api_signature">AFcWxV21C7fd0v3bYYYRCpSSRl31AqoP3QLd.JUUpDPuPpQIgT0-m401</data> <data key="paypal_express_merchantID">54Z2EE6T7PRB4</data> </entity> + <entity name="SamplePaypalPaymentsProConfig" type="paypal_paymentspro_config"> + <data key="paypal_paymentspro_parner">PayPal</data> + <data key="paypal_paymentspro_user">MksGLTest</data> + <data key="paypal_paymentspro_vendor">MksGLTest</data> + <data key="paypal_paymentspro_password">Abcd@123</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml new file mode 100644 index 0000000000000..9f4b2a6a47f19 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="PayPalPayflowProConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout-head" parameterized="true"/> + <element name="partner" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_partner" parameterized="true"/> + <element name="user" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_user" parameterized="true"/> + <element name="vendor" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_vendor" parameterized="true"/> + <element name="password" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_pwd" parameterized="true"/> + <element name="testmode" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_sandbox_flag" parameterized="true"/> + <element name="enableSolution" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_enable_paypal_payflow" parameterized="true"/> + <element name="enableVault" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_payflowpro_cc_vault_active" parameterized="true"/> + <element name="paymentGateway" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways-head" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml index 3b70bc84037ce..c66df74869e5a 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml @@ -15,6 +15,7 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country France"/> <severity value="MAJOR"/> <testCaseId value="MC-16675"/> + <group value="pr_exclude" /> <group value="paypal"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="France" stepKey="setMerchantCountry"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml index 038ee1c04c482..8286f30fd515b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-16676"/> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Hong Kong SAR China" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml index ad24d2c2c95d5..561c98131c744 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Italy"/> <severity value="MAJOR"/> <testCaseId value="MC-16677"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Italy" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml index 846f4e6dd5ae4..7c7a2e6100f1e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Japan"/> <severity value="MAJOR"/> <testCaseId value="MC-13146"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Japan" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml index b0317f9ac7a3d..05ae84d70d3a1 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Spain"/> <severity value="MAJOR"/> <testCaseId value="MC-16678"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Spain" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml index a616c0bb2c68b..2af6a73cf1a14 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-16679"/> <group value="paypal"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml index 778473abb2cc7..b46565fcc99f9 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml index 3bd778620f563..f66d2bae639dc 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml new file mode 100644 index 0000000000000..5fc0469a1b096 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest"> + <annotations> + <stories value="Stored Payment Method"/> + <title value="Delete saved with Payflow Pro credit card from customer account"/> + <description value="Delete saved with Payflow Pro credit card from customer account"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4838"/> + <group value="paypal"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminPayPalPayflowProWithValutActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalPaymentsProConfig"/> + </actionGroup> + </before> + <after> + <createData entity="RollbackPaypalPayflowPro" stepKey="rollbackPaypalPayflowProConfig"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + <!--Fill Card Data --> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal"> + <argument name="cardData" value="VisaDefaultCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <!-- 2nd time order--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront2"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate2"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage2"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask2ndTime"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad2ndTime"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod2"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection2"/> + <!--Fill Card Data --> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal2"> + <argument name="cardData" value="Visa3DSecureCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData2ndTime"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder2"/> + <!-- Go to My Account --> + <!-- Open My Account > Stored Payment Methods --> + <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> + <waitForPageLoad stepKey="waitForSideBarPageLoad2ndTime"/> + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu2"> + <argument name="menu" value="Stored Payment Methods"/> + </actionGroup> + <!-- Assert Card number that ends with 1111 and exp Date--> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod"> + <argument name="card" value="VisaDefaultCardInfo"/> + </actionGroup> + <!-- Assert Card number that ends with 0002 and exp Date--> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod2"> + <argument name="card" value="Visa3DSecureCardInfo"/> + </actionGroup> + <!-- Delete second card--> + <actionGroup ref="StorefrontDeleteStoredPaymentMethodActionGroup" stepKey="deleteStoredCard"> + <argument name="card" value="Visa3DSecureCardInfo"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml new file mode 100644 index 0000000000000..f16c1df71a129 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Edit Order from Admin with saved within PayPal Payflow Pro credit card for Registered Customer"/> + <description value="Edit Order from Admin with saved within PayPal Payflow Pro credit card for Registered Customer"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5107"/> + <group value="paypal"/> + <group value="payflowpro"/> + </annotations> + <before> + <!--Create a customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create simple product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"/> + <!-- Login to admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Configure Paypal payflowpro--> + <actionGroup ref="AdminPayPalPayflowProWithValutActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalPaymentsProConfig"/> + </actionGroup> + </before> + <after> + <!-- Disable payflowpro--> + <createData entity="RollbackPaypalPayflowPro" stepKey="rollbackPaypalPayflowProConfig"/> + <!-- Delete product and customer--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + <!--Fill Card Data and place an order--> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal"> + <argument name="cardData" value="VisaDefaultCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <!-- Grab order number--> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Navigate to admin order grid and filter the order--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <!-- Click on edit--> + <actionGroup ref="AdminEditOrderActionGroup" stepKey="openOrderForEdit"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <!-- Select stored card and submit order--> + <conditionalClick selector="{{AdminOrderFormPaymentSection.storedCard}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> + <!-- Filter order--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderByIdAgain"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <!--verify order status is canceled--> + <click selector="{{AdminOrdersGridSection.secondRow}}" stepKey="clickSecondOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <see userInput="Canceled" selector="{{AdminOrderDetailsInformationSection.orderStatus}}" stepKey="seeOrderStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml new file mode 100644 index 0000000000000..42606464c4a9d --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.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="EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest"> + <annotations> + <features value="PayPal"/> + <stories value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <title value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <description value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <severity value="MAJOR"/> + <testCaseId value="AC-6951"/> + </annotations> + <before> + <!--Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- New Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">1</field> + </createData> + <actionGroup ref="AdminPayPalExpressCheckoutEnableActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalExpressConfig2"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + <magentoCLI command="config:set paypal/general/merchant_country US" stepKey="setMerchantCountry"/> + <magentoCLI command="config:set payment/paypal_express/active 0" stepKey="disablePayPalExpress"/> + <magentoCLI command="config:set payment/wps_express/active 0" stepKey="disableWPSExpress"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + <!--Go to cart page--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="gotoCart"/> + <!-- Click on Paypal paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"> + <argument name="elementNumber" value="1"/> + </actionGroup> + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"/> + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> + <!--Click PayPal button and go back to Magento site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" stepKey="goBackToMagentoSite"/> + <actionGroup ref="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup" stepKey="selectShippingMethod"> + <argument name="shippingMethod" value="Free - $0.00"/> + </actionGroup> + <actionGroup ref="StorefrontPlaceOrderOnOrderReviewPageActionGroup" stepKey="clickPlaceOrderBtn"/> + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Go to Admin and check order information--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGrid"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php index c164d832ad460..25f99bf7acc38 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php @@ -32,6 +32,7 @@ abstract class ExpressTest extends TestCase /** @var Express */ protected $model; + /** @var string */ protected $name = ''; /** @var Session|MockObject */ @@ -75,7 +76,7 @@ abstract class ExpressTest extends TestCase protected function setUp(): void { - $this->markTestIncomplete(); + $this->markTestSkipped(); $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); $this->config = $this->createMock(Config::class); $this->request = $this->createMock(Http::class); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php index affb335491c52..b2179fb32fe58 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php @@ -137,6 +137,15 @@ public function validationDataProvider() 'response' => new DataObject(), 'configValue' => '1', ], + [ + 'expectedResult' => true, + 'response' => new DataObject( + [ + 'cvv2match' => 'N', + ] + ), + 'configValue' => '0', + ], ]; } } diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md index 71588599a5ecd..02b1cd3c3f93a 100644 --- a/app/code/Magento/PaypalCaptcha/README.md +++ b/app/code/Magento/PaypalCaptcha/README.md @@ -1 +1 @@ -The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. \ No newline at end of file +The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. diff --git a/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php new file mode 100644 index 0000000000000..4611cc5f04876 --- /dev/null +++ b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Plugin; + +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; +use Magento\Persistent\Helper\Session as PersistentSession; + +class LoginAsCustomerCleanUp +{ + /** + * @var PersistentSession + */ + private $persistentSession; + + /** + * @param PersistentSession $persistentSession + */ + public function __construct(PersistentSession $persistentSession) + { + $this->persistentSession = $persistentSession; + } + + /** + * Disable persistence for sales representative login + * + * @param AuthenticateCustomerBySecretInterface $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(AuthenticateCustomerBySecretInterface $subject) + { + if ($this->persistentSession->isPersistent()) { + $this->persistentSession->getSession()->removePersistentCookie(); + } + } +} diff --git a/app/code/Magento/Persistent/README.md b/app/code/Magento/Persistent/README.md index abbf4482eda6c..3d2f19e4fc91b 100644 --- a/app/code/Magento/Persistent/README.md +++ b/app/code/Magento/Persistent/README.md @@ -9,12 +9,14 @@ checkbox during first login. ## Installation Before installing this module, note that the Magento_Persistent is dependent on the following modules: + - `Magento_Checkout` - `Magento_PageCache` The Magento_Persistent module creates the `persistent_session` table in the database. This module modifies the following tables in the database: + - `quote` - adds column `is_persistent` All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. @@ -50,12 +52,14 @@ For more information about a layout in Magento 2, see the [Layout documentation] ## Additional information More information can get at articles: + - [Persistent Shopping Cart](https://docs.magento.com/user-guide/configuration/customers/persistent-shopping-cart.html) - [Persistent Cart](https://experienceleague.adobe.com/docs/commerce-admin/stores-sales/point-of-purchase/cart/cart-persistent.html) ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `persistent_clear_expired` - clear expired persistent sessions [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml index 1f944432ac1d1..816b63d8748ec 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-92453"/> <group value="persistent"/> + <group value="cloud"/> </annotations> <before> <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml index f094c4f07475d..b98ec88222e2d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-99025"/> <useCaseId value="MAGETWO-98620"/> <group value="persistent"/> + <group value="cloud"/> </annotations> <before> <!--Enabled The Persistent Shopping Cart feature --> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml index ebc3aee9d2fd2..3dcd52629e1fc 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-10800"/> <group value="persistent"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable Persistence--> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml index 814ca782e28d2..e47ab9187010c 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="AC-2619"/> <group value="persistent"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable Persistence--> diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php new file mode 100644 index 0000000000000..43519f362f590 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Model\Plugin; + +use Magento\Persistent\Model\Plugin\LoginAsCustomerCleanUp; +use Magento\Persistent\Helper\Session as PersistentSession; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LoginAsCustomerCleanUpTest extends TestCase +{ + /** + * @var LoginAsCustomerCleanUp + */ + protected $plugin; + + /** + * @var MockObject + */ + protected $subjectMock; + + /** + * @var MockObject + */ + protected $persistentSessionMock; + + /** + * @var MockObject + */ + protected $persistentSessionModelMock; + + protected function setUp(): void + { + $this->persistentSessionMock = $this->createMock(PersistentSession::class); + $this->persistentSessionModelMock = $this->createMock(\Magento\Persistent\Model\Session::class); + $this->persistentSessionMock->method('getSession')->willReturn($this->persistentSessionModelMock); + $this->subjectMock = $this->createMock(AuthenticateCustomerBySecretInterface::class); + $this->plugin = new LoginAsCustomerCleanUp($this->persistentSessionMock); + } + + public function testBeforeExecute() + { + $this->persistentSessionMock->expects($this->once())->method('isPersistent')->willReturn(true); + $this->persistentSessionModelMock->expects($this->once())->method('removePersistentCookie'); + $result = $this->plugin->afterExecute($this->subjectMock); + $this->assertEquals(null, $result); + } +} diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index 5a8ff5d7f3d5f..6c943c4b37f82 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -14,6 +14,9 @@ "magento/module-quote": "*", "magento/module-store": "*" }, + "suggest": { + "magento/module-login-as-customer-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index 3351963231277..498b59b7e4c45 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -57,4 +57,7 @@ <argument name="shippingAssignmentProcessor" xsi:type="object">Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy</argument> </arguments> </type> + <type name="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface"> + <plugin name="login_as_customer_cleanup" type="Magento\Persistent\Model\Plugin\LoginAsCustomerCleanUp" /> + </type> </config> diff --git a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php index 988e5e91e1e81..a77ca851ac746 100644 --- a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php +++ b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php @@ -7,7 +7,6 @@ namespace Magento\ProductAlert\Model\Mailing; -use Magento\Framework\App\Area; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Helper\Data; @@ -25,15 +24,11 @@ use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Website; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\View\DesignInterface; /** * Class for mailing Product Alerts * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ class AlertProcessor { @@ -85,11 +80,6 @@ class AlertProcessor */ private $errorEmailSender; - /** - * @var DesignInterface - */ - private $design; - /** * @param EmailFactory $emailFactory * @param PriceCollectionFactory $priceCollectionFactory @@ -100,7 +90,6 @@ class AlertProcessor * @param ProductSalability $productSalability * @param StoreManagerInterface $storeManager * @param ErrorEmailSender $errorEmailSender - * @param DesignInterface|null $design */ public function __construct( EmailFactory $emailFactory, @@ -111,8 +100,7 @@ public function __construct( Data $catalogData, ProductSalability $productSalability, StoreManagerInterface $storeManager, - ErrorEmailSender $errorEmailSender, - DesignInterface $design = null + ErrorEmailSender $errorEmailSender ) { $this->emailFactory = $emailFactory; $this->priceCollectionFactory = $priceCollectionFactory; @@ -123,8 +111,6 @@ public function __construct( $this->productSalability = $productSalability; $this->storeManager = $storeManager; $this->errorEmailSender = $errorEmailSender; - $this->design = $design ?: ObjectManager::getInstance() - ->get(DesignInterface::class); } /** @@ -159,12 +145,6 @@ public function process(string $alertType, array $customerIds, int $websiteId): */ private function processAlerts(string $alertType, array $customerIds, int $websiteId): array { - //Set the current design theme - $this->design->setDesignTheme( - $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND), - Area::AREA_FRONTEND - ); - /** @var Email $email */ $email = $this->emailFactory->create(); $email->setType($alertType); diff --git a/app/code/Magento/ProductAlert/README.md b/app/code/Magento/ProductAlert/README.md index 76ee9f8066cd5..1d54f5e7b811b 100644 --- a/app/code/Magento/ProductAlert/README.md +++ b/app/code/Magento/ProductAlert/README.md @@ -5,16 +5,18 @@ This module enables product alerts, which allow customers to sign up for emails ## Installation Before installing this module, note that the Magento_ProductAlert is dependent on the following modules: + - `Magento_Catalog` - `Magento_Customer` The Magento_ProductAlert module creates the following tables in the database: + - `product_alert_price` - `product_alert_stock` All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation. +The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation. For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). @@ -27,6 +29,7 @@ Extension developers can interact with the Magento_ProductAlert module. For more ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `catalog_product_view` - `productalert_unsubscribe_email` @@ -35,13 +38,14 @@ For more information about a layout in Magento 2, see the [Layout documentation] ## Additional information More information can get at articles: + - [Product Alerts](https://docs.magento.com/user-guide/catalog/inventory-product-alerts.html) - [Product Alert Run Settings](https://docs.magento.com/user-guide/catalog/inventory-product-alert-run-settings.html) ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `catalog_product_alert` - send product alerts to customers [Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). - diff --git a/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php new file mode 100644 index 0000000000000..c9b8e331ad7a6 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\ProductAlert\Model\PriceFactory; +use Magento\ProductAlert\Model\ResourceModel\Price; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +class PriceAlert implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'customer_id' => null, + 'product_id' => null, + 'store_id' => 1, + 'website_id' => null, + 'price' => 11, + ]; + + /** + * @var PriceFactory + */ + private PriceFactory $factory; + + /** + * @var Price + */ + private Price $resourceModel; + + /** + * @var StoreManagerInterface + */ + private StoreManagerInterface $storeManager; + + /** + * @param PriceFactory $factory + * @param Price $resourceModel + * @param StoreManagerInterface $storeManager + */ + public function __construct( + PriceFactory $factory, + Price $resourceModel, + StoreManagerInterface $storeManager + ) { + $this->factory = $factory; + $this->resourceModel = $resourceModel; + $this->storeManager = $storeManager; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'customer_id' => (int) Customer ID. Required. + * 'product_id' => (int) Product ID. Required. + * 'store_id' => (int) Store ID. Optional. Default: default store. + * 'website_id' => (int) Website ID. Optional. Default: default website. + * 'price' => (float) Initial Price. Optional. Default: 11. + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId(); + $model = $this->factory->create(); + $model->addData($data); + $this->resourceModel->save($model); + + return $model; + } +} diff --git a/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php new file mode 100644 index 0000000000000..09d47ddc2b1aa --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\ProductAlert\Model\StockFactory; +use Magento\ProductAlert\Model\ResourceModel\Stock; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +class StockAlert implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'customer_id' => null, + 'product_id' => null, + 'store_id' => 1, + 'website_id' => null, + 'status' => 0, + ]; + + /** + * @var StockFactory + */ + private StockFactory $factory; + + /** + * @var Stock + */ + private Stock $resourceModel; + + /** + * @var StoreManagerInterface + */ + private StoreManagerInterface $storeManager; + + /** + * @param StockFactory $factory + * @param Stock $resourceModel + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StockFactory $factory, + Stock $resourceModel, + StoreManagerInterface $storeManager + ) { + $this->factory = $factory; + $this->resourceModel = $resourceModel; + $this->storeManager = $storeManager; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'customer_id' => (int) Customer ID. Required. + * 'product_id' => (int) Product ID. Required. + * 'store_id' => (int) Store ID. Optional. Default: default store. + * 'website_id' => (int) Website ID. Optional. Default: default website. + * 'status' => (int) Alert Status. Optional. Default: 0. + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId(); + $model = $this->factory->create(); + $model->addData($data); + $this->resourceModel->save($model); + + return $model; + } +} diff --git a/app/code/Magento/ProductVideo/README.md b/app/code/Magento/ProductVideo/README.md index a36a1f777c655..f3b9926dd111b 100644 --- a/app/code/Magento/ProductVideo/README.md +++ b/app/code/Magento/ProductVideo/README.md @@ -5,6 +5,7 @@ This module implements functionality related with linking video files from exter ## Installation Before installing this module, note that the Magento_ProductAlert is dependent on the following modules: + - `Magento_Catalog` - `Magento_Backend` @@ -25,6 +26,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout` - `catalog_product_new` - `view/frontend/layout` @@ -35,6 +37,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `product_form` For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). @@ -42,5 +45,6 @@ For information about a UI component in Magento 2, see [Overview of UI component ## Additional information More information can get at articles: + - [Learn how to add Product Video](https://docs.magento.com/user-guide/catalog/product-video.html) - [Learn how to configure Product Video](https://developer.adobe.com/commerce/frontend-core/guide/themes/product-video/) diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml index 5b346040db818..d3ce3159187c5 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42645"/> <useCaseId value="MC-42448"/> <group value="productVideo"/> + <group value="cloud"/> </annotations> <before> <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setYoutubeApiKeyConfig"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml new file mode 100644 index 0000000000000..3086ee1979f1f --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="Storefront product video autoplay on gallery full screen mode"/> + <title value="Storefront product video gets auto played on gallery full screen mode"/> + <description value="Storefront product video autoplay on selecting the video by clicking video thumbnail in + gallery full screen mode"/> + <severity value="MAJOR"/> + <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> + <actionGroup ref="StorefrontProductPageOpenImageFullscreenActionGroup" stepKey="openGalleryFullScreen"> + <argument name="imageNumber" value="1"/> + </actionGroup> + <conditionalClick selector="{{StorefrontProductMediaSection.fotoramaImageThumbnail('2')}}" + dependentSelector="{{StorefrontProductMediaSection.fotoramaImageThumbnailActive('2')}}" + visible="false" stepKey="clickOnVideoThumbnail"/> + <wait stepKey="waitTenSecondsToPlayVideo" time="10"/> + <!-- On clicking video thumbnail, assert the video iframe is loaded with autoplay attribute --> + <seeElementInDOM selector="iframe" stepKey="AssertVideoIsPlayed"/> + <grabAttributeFrom selector="iframe" userInput="allow" stepKey="grabAllowAttribute"/> + <assertStringContainsString stepKey="assertAllowAttribute"> + <actualResult type="string">$grabAllowAttribute</actualResult> + <expectedResult type="string">autoplay</expectedResult> + </assertStringContainsString> + <actionGroup ref="StorefrontProductPageCloseFullscreenGalleryActionGroup" stepKey="closeGalleryFullScreen"/> + </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 670d91febe9f7..a8a168c03aa95 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 @@ -558,7 +558,7 @@ define([ } if (this.isFullscreen && this.fotoramaItem.data('fotorama').activeFrame.i === number) { - this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click'); + this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click'); } }, @@ -700,7 +700,7 @@ define([ if (activeIndexIsBase && number === 1 && $(window).width() > this.MobileMaxWidth) { setTimeout($.proxy(function () { fotorama.requestFullScreen(); - this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click'); + this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click'); this.Base = false; }, this), 50); } diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index 6f8a44dff464c..9ed4f5ecd516b 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -3,14 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; -use Magento\Quote\Model\Quote\Address\BillingAddressPersister; -use Psr\Log\LoggerInterface as Logger; use Magento\Quote\Api\BillingAddressManagementInterface; -use Magento\Framework\App\ObjectManager; +use Magento\Quote\Api\Data\AddressInterface; +use Psr\Log\LoggerInterface as Logger; /** * Quote billing address write service object. @@ -25,14 +26,14 @@ class BillingAddressManagement implements BillingAddressManagementInterface protected $addressValidator; /** - * Logger. + * Logger object. * * @var Logger */ protected $logger; /** - * Quote repository. + * Quote repository object. * * @var \Magento\Quote\Api\CartRepositoryInterface */ @@ -72,10 +73,14 @@ public function __construct( * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address, $useForShipping = false) + public function assign($cartId, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); + + // validate the address + $this->addressValidator->validateWithExistingAddress($quote, $address); + $address->setCustomerId($quote->getCustomerId()); $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($address); @@ -104,6 +109,7 @@ public function get($cartId) * * @return \Magento\Quote\Model\ShippingAddressAssignment * @deprecated 101.0.0 + * @see \Magento\Quote\Model\ShippingAddressAssignment */ private function getShippingAddressAssignment() { diff --git a/app/code/Magento/Quote/Model/Cart/ProductReader.php b/app/code/Magento/Quote/Model/Cart/ProductReader.php index 6a333e8b9b795..1dd127977d686 100644 --- a/app/code/Magento/Quote/Model/Cart/ProductReader.php +++ b/app/code/Magento/Quote/Model/Cart/ProductReader.php @@ -62,6 +62,7 @@ public function loadProducts(array $skus, int $storeId): void $this->productCollection->addFieldToFilter(ProductInterface::SKU, ['in' => $skus]); $this->productCollection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $this->productCollection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); + $this->productCollection->addOptionsToResult(); $this->productCollection->load(); foreach ($this->productCollection->getItems() as $productItem) { $this->productsBySku[$productItem->getData(ProductInterface::SKU)] = $productItem; diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 572d87d5f4bec..7270734e3ff4e 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -985,6 +985,8 @@ public function assignCustomerWithAddressChange( /** * Define customer object * + * Important: This method also copies customer data to quote and removes quote addresses + * * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @return $this */ diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index 6fdb70350ed72..9f28f52adee97 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -5,12 +5,13 @@ */ namespace Magento\Quote\Model\Quote\Address; +use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Framework\Exception\InputException; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Api\Data\AddressInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\QuoteAddressValidator; -use Magento\Customer\Api\AddressRepositoryInterface; /** * Saves billing address for quotes. @@ -47,7 +48,7 @@ public function __construct( * @param bool $useForShipping * @return void * @throws NoSuchEntityException - * @throws InputException + * @throws InputException|LocalizedException */ public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { @@ -55,8 +56,6 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; - $addressData = []; - if ($useForShipping) { $shippingAddress = $address; } @@ -64,13 +63,13 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi if ($customerAddressId) { try { $addressData = $this->addressRepository->getById($customerAddressId); + $address = $quote->getBillingAddress()->importCustomerAddressData($addressData); + if ($useForShipping) { + $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData); + $shippingAddress->setSaveInAddressBook($saveInAddressBook); + } } catch (NoSuchEntityException $e) { - // do nothing if customer is not found by id - } - $address = $quote->getBillingAddress()->importCustomerAddressData($addressData); - if ($useForShipping) { - $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData); - $shippingAddress->setSaveInAddressBook($saveInAddressBook); + $address->setCustomerAddressId(null); } } elseif ($quote->getCustomerId()) { $address->setEmail($quote->getCustomerEmail()); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Rate.php b/app/code/Magento/Quote/Model/Quote/Address/Rate.php index 3f96be4bd25a4..339add647c90c 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Rate.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Rate.php @@ -43,6 +43,13 @@ class Rate extends AbstractModel protected $_address; /** + * @var carrier_sort_order + */ + public $carrier_sort_order; + + /** + * Check the Quote rate + * * @return void */ protected function _construct() @@ -51,6 +58,8 @@ protected function _construct() } /** + * Set Address id with address before save + * * @return $this */ public function beforeSave() @@ -63,6 +72,8 @@ public function beforeSave() } /** + * Set address + * * @param \Magento\Quote\Model\Quote\Address $address * @return $this */ @@ -73,6 +84,8 @@ public function setAddress(\Magento\Quote\Model\Quote\Address $address) } /** + * Get Method for address + * * @return \Magento\Quote\Model\Quote\Address */ public function getAddress() @@ -81,6 +94,8 @@ public function getAddress() } /** + * Import shipping rate + * * @param \Magento\Quote\Model\Quote\Address\RateResult\AbstractResult $rate * @return $this */ diff --git a/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php new file mode 100644 index 0000000000000..3536e092d132f --- /dev/null +++ b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Address; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\QuoteAddressValidator; + +class ShippingAddressPersister +{ + /** + * @var QuoteAddressValidator + */ + private $addressValidator; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @param QuoteAddressValidator $addressValidator + * @param AddressRepositoryInterface $addressRepository + */ + public function __construct( + QuoteAddressValidator $addressValidator, + AddressRepositoryInterface $addressRepository + ) { + $this->addressValidator = $addressValidator; + $this->addressRepository = $addressRepository; + } + + /** + * Save address for shipping. + * + * @param CartInterface $quote + * @param AddressInterface $address + * @return void + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function save(CartInterface $quote, AddressInterface $address): void + { + $this->addressValidator->validateForCart($quote, $address); + $customerAddressId = $address->getCustomerAddressId(); + + $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; + if ($customerAddressId) { + try { + $addressData = $this->addressRepository->getById($customerAddressId); + $address = $quote->getShippingAddress()->importCustomerAddressData($addressData); + } catch (NoSuchEntityException $e) { + $address->setCustomerAddressId(null); + } + } elseif ($quote->getCustomerId()) { + $address->setEmail($quote->getCustomerEmail()); + } + $address->setSaveInAddressBook($saveInAddressBook); + $quote->setShippingAddress($address); + } +} diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index f0bc12f7b3a36..f8e7141b3bb00 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\CartInterface; @@ -17,35 +24,33 @@ class QuoteAddressValidator { /** - * Address factory. - * - * @var \Magento\Customer\Api\AddressRepositoryInterface + * @var AddressRepositoryInterface */ - protected $addressRepository; + protected AddressRepositoryInterface $addressRepository; /** - * Customer repository. - * - * @var \Magento\Customer\Api\CustomerRepositoryInterface + * @var CustomerRepositoryInterface */ - protected $customerRepository; + protected CustomerRepositoryInterface $customerRepository; /** + * @var Session * @deprecated 101.1.1 This class is not a part of HTML presentation layer and should not use sessions. + * @see Session */ - protected $customerSession; + protected Session $customerSession; /** * Constructs a quote shipping address validator service object. * - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository - * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository Customer repository. - * @param \Magento\Customer\Model\Session $customerSession + * @param AddressRepositoryInterface $addressRepository + * @param CustomerRepositoryInterface $customerRepository Customer repository. + * @param Session $customerSession */ public function __construct( - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, - \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - \Magento\Customer\Model\Session $customerSession + AddressRepositoryInterface $addressRepository, + CustomerRepositoryInterface $customerRepository, + Session $customerSession ) { $this->addressRepository = $addressRepository; $this->customerRepository = $customerRepository; @@ -56,10 +61,10 @@ public function __construct( * Validate address. * * @param AddressInterface $address - * @param int|null $customerId Cart belongs to + * @param int|null $customerId * @return void - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws LocalizedException The specified customer ID or address ID is not valid. + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. */ private function doValidate(AddressInterface $address, ?int $customerId): void { @@ -67,7 +72,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void if ($customerId) { $customer = $this->customerRepository->getById($customerId); if (!$customer->getId()) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer id %1', $customerId) ); } @@ -76,7 +81,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void if ($address->getCustomerAddressId()) { //Existing address cannot belong to a guest if (!$customerId) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } @@ -84,7 +89,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void try { $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid address id %1', $address->getId()) ); } @@ -94,7 +99,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void return $address->getId(); }, $this->customerRepository->getById($customerId)->getAddresses()); if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } @@ -104,29 +109,74 @@ private function doValidate(AddressInterface $address, ?int $customerId): void /** * Validates the fields in a specified address data object. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @param AddressInterface $addressData The address data object. * @return bool - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws InputException The specified address belongs to another customer. + * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid. */ - public function validate(AddressInterface $addressData) + public function validate(AddressInterface $addressData): bool { $this->doValidate($addressData, $addressData->getCustomerId()); return true; } + /** + * Validate Quest Address for guest user + * + * @param AddressInterface $address + * @param CartInterface $cart + * @return void + * @throws NoSuchEntityException + */ + private function doValidateForGuestQuoteAddress(AddressInterface $address, CartInterface $cart): void + { + //validate guest cart address + if ($address->getId() !== null) { + $old = $cart->getAddressById($address->getId()); + if ($old === false) { + throw new NoSuchEntityException( + __('Invalid quote address id %1', $address->getId()) + ); + } + } + } + /** * Validate address to be used for cart. * * @param CartInterface $cart * @param AddressInterface $address * @return void - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws InputException The specified address belongs to another customer. + * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid. */ public function validateForCart(CartInterface $cart, AddressInterface $address): void { - $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + if ($cart->getCustomerIsGuest()) { + $this->doValidateForGuestQuoteAddress($address, $cart); + } + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : (int) $cart->getCustomer()->getId()); + } + + /** + * Validate address id to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validateWithExistingAddress(CartInterface $cart, AddressInterface $address): void + { + // check if address belongs to quote. + if ($address->getId() !== null) { + $old = $cart->getAddressesCollection()->getItemById($address->getId()); + if ($old === null) { + throw new NoSuchEntityException( + __('Invalid quote address id %1', $address->getId()) + ); + } + } } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index dc0858f183809..3391ea6a29124 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -26,6 +26,7 @@ use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Validator\Exception as ValidatorException; use Magento\Payment\Model\Method\AbstractMethod; use Magento\Quote\Api\CartManagementInterface; @@ -50,7 +51,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class QuoteManagement implements CartManagementInterface +class QuoteManagement implements CartManagementInterface, ResetAfterRequestInterface { private const LOCK_PREFIX = 'PLACE_ORDER_'; @@ -774,4 +775,12 @@ private function rollbackAddresses( throw new \Exception($message, 0, $e); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->addressesToSync = []; + } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index b1bef834197aa..776479a4773f8 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -14,12 +14,13 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartInterfaceFactory; use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; -use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\QuoteRepository\LoadHandler; +use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; use Magento\Store\Model\StoreManagerInterface; @@ -29,7 +30,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteRepository implements CartRepositoryInterface +class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInterface { /** * @var Quote[] @@ -44,6 +45,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteFactory * @deprecated 101.1.2 + * @see no longer used */ protected $quoteFactory; @@ -55,6 +57,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteCollection * @deprecated 101.0.0 + * @see $quoteCollectionFactory */ protected $quoteCollection; @@ -98,7 +101,7 @@ class QuoteRepository implements CartRepositoryInterface * * @param QuoteFactory $quoteFactory * @param StoreManagerInterface $storeManager - * @param QuoteCollection $quoteCollection + * @param QuoteCollection $quoteCollection Deprecated. Use $quoteCollectionFactory * @param CartSearchResultsInterfaceFactory $searchResultsDataFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface|null $collectionProcessor @@ -127,6 +130,15 @@ public function __construct( $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class); } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->quotesById = []; + $this->quotesByCustomerId = []; + } + /** * @inheritdoc */ @@ -198,7 +210,6 @@ public function save(CartInterface $quote) } } } - $this->getSaveHandler()->save($quote); unset($this->quotesById[$quote->getId()]); unset($this->quotesByCustomerId[$quote->getCustomerId()]); @@ -268,6 +279,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) * @param QuoteCollection $collection The quote collection. * @return void * @deprecated 101.0.0 + * @see no longer used * @throws InputException The specified filter group or quote collection does not exist. */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCollection $collection) @@ -288,7 +300,6 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol * Get new SaveHandler dependency for application code. * * @return SaveHandler - * @deprecated 100.1.0 */ private function getSaveHandler() { @@ -302,7 +313,6 @@ private function getSaveHandler() * Get load handler instance. * * @return LoadHandler - * @deprecated 100.1.0 */ private function getLoadHandler() { diff --git a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php index 12a71648690d4..12d155de5b017 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php @@ -7,35 +7,47 @@ namespace Magento\Quote\Model\QuoteRepository; -use Magento\Quote\Api\Data\CartInterface; +use Magento\Backend\Model\Session\Quote as QuoteSession; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote\Address\BillingAddressPersister; +use Magento\Quote\Model\Quote\Address\ShippingAddressPersister; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister; +use Magento\Quote\Model\ResourceModel\Quote; /** * Handler for saving quote. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SaveHandler { /** - * @var \Magento\Quote\Model\Quote\Item\CartItemPersister + * @var CartItemPersister */ private $cartItemPersister; /** - * @var \Magento\Quote\Model\Quote\Address\BillingAddressPersister + * @var BillingAddressPersister */ private $billingAddressPersister; /** - * @var \Magento\Quote\Model\ResourceModel\Quote + * @var Quote */ private $quoteResourceModel; /** - * @var \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister + * @var ShippingAssignmentPersister */ private $shippingAssignmentPersister; @@ -50,20 +62,34 @@ class SaveHandler private $quoteAddressFactory; /** - * @param \Magento\Quote\Model\ResourceModel\Quote $quoteResource - * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister - * @param \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister - * @param \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister - * @param AddressRepositoryInterface $addressRepository + * @var ShippingAddressPersister + */ + private $shippingAddressPersister; + + /** + * @var QuoteSession + */ + private $quoteSession; + + /** + * @param Quote $quoteResource + * @param CartItemPersister $cartItemPersister + * @param BillingAddressPersister $billingAddressPersister + * @param ShippingAssignmentPersister $shippingAssignmentPersister + * @param AddressRepositoryInterface|null $addressRepository * @param AddressInterfaceFactory|null $addressFactory + * @param ShippingAddressPersister|null $shippingAddressPersister + * @param QuoteSession|null $quoteSession */ public function __construct( - \Magento\Quote\Model\ResourceModel\Quote $quoteResource, - \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, - \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister, - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister, + Quote $quoteResource, + CartItemPersister $cartItemPersister, + BillingAddressPersister $billingAddressPersister, + ShippingAssignmentPersister $shippingAssignmentPersister, AddressRepositoryInterface $addressRepository = null, - AddressInterfaceFactory $addressFactory = null + AddressInterfaceFactory $addressFactory = null, + ShippingAddressPersister $shippingAddressPersister = null, + QuoteSession $quoteSession = null ) { $this->quoteResourceModel = $quoteResource; $this->cartItemPersister = $cartItemPersister; @@ -71,8 +97,11 @@ public function __construct( $this->shippingAssignmentPersister = $shippingAssignmentPersister; $this->addressRepository = $addressRepository ?: ObjectManager::getInstance()->get(AddressRepositoryInterface::class); - $this->quoteAddressFactory = $addressFactory ?:ObjectManager::getInstance() + $this->quoteAddressFactory = $addressFactory ?: ObjectManager::getInstance() ->get(AddressInterfaceFactory::class); + $this->shippingAddressPersister = $shippingAddressPersister + ?: ObjectManager::getInstance()->get(ShippingAddressPersister::class); + $this->quoteSession = $quoteSession ?: ObjectManager::getInstance()->get(QuoteSession::class); } /** @@ -81,18 +110,16 @@ public function __construct( * @param CartInterface $quote * @return CartInterface * @throws InputException - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws CouldNotSaveException + * @throws LocalizedException */ public function save(CartInterface $quote) { - /** @var \Magento\Quote\Model\Quote $quote */ // Quote Item processing $items = $quote->getItems(); if ($items) { foreach ($items as $item) { - /** @var \Magento\Quote\Model\Quote\Item $item */ if (!$item->isDeleted()) { $quote->setLastAddedItem($this->cartItemPersister->save($quote, $item)); } elseif (count($items) === 1) { @@ -104,33 +131,50 @@ public function save(CartInterface $quote) // Billing Address processing $billingAddress = $quote->getBillingAddress(); - if ($billingAddress) { - if ($billingAddress->getCustomerAddressId()) { - try { - $this->addressRepository->getById($billingAddress->getCustomerAddressId()); - } catch (NoSuchEntityException $e) { - $billingAddress->setCustomerAddressId(null); - } - } - + $this->processAddress($billingAddress); $this->billingAddressPersister->save($quote, $billingAddress); } + // Shipping Address processing + if ($this->quoteSession->getData(('reordered'))) { + $shippingAddress = $this->processAddress($quote->getShippingAddress()); + $this->shippingAddressPersister->save($quote, $shippingAddress); + } + $this->processShippingAssignment($quote); $this->quoteResourceModel->save($quote->collectTotals()); return $quote; } + /** + * Process address for customer address Id + * + * @param AddressInterface $address + * @return AddressInterface + * @throws LocalizedException + */ + private function processAddress(AddressInterface $address): AddressInterface + { + if ($address->getCustomerAddressId()) { + try { + $this->addressRepository->getById($address->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $address->setCustomerAddressId(null); + } + } + return $address; + } + /** * Process shipping assignment * - * @param \Magento\Quote\Model\Quote $quote + * @param CartInterface $quote * @return void * @throws InputException */ - private function processShippingAssignment($quote) + private function processShippingAssignment(CartInterface $quote) { // Shipping Assignments processing $extensionAttributes = $quote->getExtensionAttributes(); diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index b59737bff988b..6e27625283bec 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -127,6 +127,16 @@ protected function _construct() $this->_init(QuoteItem::class, ResourceQuoteItem::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_productIds = []; + $this->_quote = null; + } + /** * Retrieve store Id (From Quote) * diff --git a/app/code/Magento/Quote/README.md b/app/code/Magento/Quote/README.md index 4c3cf42359f0c..439b902ca9946 100644 --- a/app/code/Magento/Quote/README.md +++ b/app/code/Magento/Quote/README.md @@ -7,6 +7,7 @@ This module provides customer cart management functionality. The Magento_Quote module is one of the base Magento 2 modules. You cannot disable or uninstall this module. The Magento_Quote module creates the following table in the database: + - `quote` - `quote_address` - `quote_item` @@ -27,6 +28,7 @@ Extension developers can interact with the Magento_Quote module. For more inform ### Events The module dispatches the following events: + - `sales_quote_address_collection_load_after` event in the `\Magento\Quote\Model\ResourceModel\Quote\Address\Collection::_afterLoad` method. Parameters: - `quote_address_collection` is a `$this` object (`Magento\Quote\Model\ResourceModel\Quote\Address\Collection` class) @@ -108,7 +110,7 @@ The module dispatches the following events: - `sales_quote_item_collection_products_after_load` event in the `\Magento\Quote\Model\QuoteManagement::_assignProducts` method. Parameters: - `collection` is a product collection object (`\Magento\Catalog\Model\ResourceModel\Product\Collection` class) -For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Public APIs @@ -169,7 +171,7 @@ For information about an event in Magento 2, see [Events and observers](https:// - `\Magento\Quote\Api\ChangeQuoteControlInterface` - checks if user is allowed to change the quote - + #### Guest - `\Magento\Quote\Api\GuestBillingAddressManagementInterface` @@ -180,7 +182,7 @@ For information about an event in Magento 2, see [Events and observers](https:// - gets lists items that are assigned to a specified quote - add/update the specified cart guest item - removes the specified item from the specified quote - + - `\Magento\Quote\Api\GuestCouponManagementInterface` - gets coupon for a specified quote by quote ID - adds a coupon by code to a specified quote @@ -205,7 +207,7 @@ For information about an event in Magento 2, see [Events and observers](https:// - `\Magento\Quote\Api\GuestCartRepositoryInterface` - gets quote by quote ID for guest user - + - `\Magento\Quote\Api\GuestCartTotalManagementInterface` - sets shipping/billing methods and additional data for a quote and collect totals for guest @@ -237,7 +239,7 @@ For information about an event in Magento 2, see [Events and observers](https:// - returns information for the quote for a specified customer - assigns a specified customer to a specified shopping quote - places an order for a specified quote - + - `\Magento\Quote\Api\CartRepositoryInterface` - gets quote by quote ID - gets list carts that match specified search criteria @@ -252,7 +254,7 @@ For information about an event in Magento 2, see [Events and observers](https:// - `\Magento\Quote\Api\CartTotalRepositoryInterface` - gets quote totals by quote ID - + - `\Magento\Quote\Api\CouponManagementInterface` - gets coupon for a specified quote by quote ID - adds a coupon by code to a specified quote @@ -278,20 +280,19 @@ For information about an event in Magento 2, see [Events and observers](https:// - `\Magento\Quote\Model\ShippingMethodManagementInterface` - sets the carrier and shipping methods codes for a specified quote - gets the selected shipping method for a specified quote - + #### Model - + - `\Magento\Quote\Model\Quote\Address\FreeShippingInterface` - checks if is a free shipping - `\Magento\Quote\Model\Quote\Address\RateCollectorInterface` - retrieves all methods for supplied shipping data - + - `\Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface` - converts masked quote ID to the quote ID (entity ID) - `\Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface` - converts quote ID to the masked quote ID - -For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). diff --git a/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php new file mode 100644 index 0000000000000..ab6bf0a3ff895 --- /dev/null +++ b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +/** + * Persist quote id mask + */ +class QuoteIdMask implements DataFixtureInterface +{ + private const FIELD_CART_ID = 'cart_id'; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private QuoteIdMaskResourceModel $quoteIdMaskResourceModel; + + /** + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data[self::FIELD_CART_ID])) { + throw new InvalidArgumentException(__('"%field" is required', ['field' => self::FIELD_CART_ID])); + } + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($data[self::FIELD_CART_ID]); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + + return $quoteIdMask; + } +} diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index ff183e3150894..98c55175318c9 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -97,8 +97,9 @@ <column name="store_id"/> <column name="is_active"/> </index> - <index referenceId="QUOTE_STORE_ID" indexType="btree"> + <index referenceId="QUOTE_STORE_ID_UPDATED_AT" indexType="btree"> <column name="store_id"/> + <column name="updated_at"/> </index> </table> <table name="quote_address" resource="checkout" engine="innodb" comment="Sales Flat Quote Address"> diff --git a/app/code/Magento/Quote/etc/db_schema_whitelist.json b/app/code/Magento/Quote/etc/db_schema_whitelist.json index 5667a9a5b4600..9e1f8ce164b61 100644 --- a/app/code/Magento/Quote/etc/db_schema_whitelist.json +++ b/app/code/Magento/Quote/etc/db_schema_whitelist.json @@ -53,7 +53,8 @@ }, "index": { "QUOTE_CUSTOMER_ID_STORE_ID_IS_ACTIVE": true, - "QUOTE_STORE_ID": true + "QUOTE_STORE_ID": true, + "QUOTE_STORE_ID_UPDATED_AT": true }, "constraint": { "PRIMARY": true, @@ -121,7 +122,9 @@ "vat_is_valid": true, "vat_request_id": true, "vat_request_date": true, - "vat_request_success": true + "vat_request_success": true, + "validated_country_code": true, + "validated_vat_number": true }, "index": { "QUOTE_ADDRESS_QUOTE_ID": true diff --git a/app/code/Magento/Quote/i18n/en_US.csv b/app/code/Magento/Quote/i18n/en_US.csv index 54b7edbd7fd15..483b29a9fdbce 100644 --- a/app/code/Magento/Quote/i18n/en_US.csv +++ b/app/code/Magento/Quote/i18n/en_US.csv @@ -69,6 +69,7 @@ Carts,Carts "Validated Country Code","Validated Country Code" "Validated Vat Number","Validated Vat Number" "Invalid Quote Item id %1","Invalid Quote Item id %1" +"Invalid quote address id %1","Invalid quote address id %1" "Number above 0 is required for the limit","Number above 0 is required for the limit" "Please select a valid rate limit period in seconds: %1.","Please select a valid rate limit period in seconds: %1." "Identity type not found","Identity type not found" diff --git a/app/code/Magento/QuoteAnalytics/README.md b/app/code/Magento/QuoteAnalytics/README.md index c7abc18ffccbe..e9e220549ab44 100644 --- a/app/code/Magento/QuoteAnalytics/README.md +++ b/app/code/Magento/QuoteAnalytics/README.md @@ -5,6 +5,7 @@ This module configures data definitions for a data collection related to the Quo ## Installation Before installing this module, note that the Magento_QuoteAnalytics is dependent on the following modules: + - `Magento_Quote` - `Magento_Analytics` @@ -15,5 +16,6 @@ For information about a module installation in Magento 2, see [Enable or disable ## Additional data More information can get at articles: + - [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/) - [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php index e15b7324ce24b..7fdce5245b475 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php @@ -20,13 +20,21 @@ class CreateBuyRequest */ private $dataObjectFactory; + /** + * @var CreateBuyRequestDataProviderInterface[] + */ + private $providers; + /** * @param DataObjectFactory $dataObjectFactory + * @param array $providers */ public function __construct( - DataObjectFactory $dataObjectFactory + DataObjectFactory $dataObjectFactory, + array $providers = [] ) { $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; } /** @@ -39,21 +47,30 @@ public function __construct( public function execute(float $qty, array $customizableOptionsData): DataObject { $customizableOptions = []; + $enteredOptions = []; foreach ($customizableOptionsData as $customizableOption) { if (isset($customizableOption['value_string'])) { - $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue( - $customizableOption['value_string'] - ); + if (!is_numeric($customizableOption['id'])) { + $enteredOptions[$customizableOption['id']] = $customizableOption['value_string']; + } else { + $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue( + $customizableOption['value_string'] + ); + } } } - $dataArray = [ - 'data' => [ + $requestData = [ + [ 'qty' => $qty, - 'options' => $customizableOptions, - ], + 'options' => $customizableOptions + ] ]; - return $this->dataObjectFactory->create($dataArray); + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($enteredOptions); + } + + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } /** diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..af52c2869e907 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +interface CreateBuyRequestDataProviderInterface +{ + /** + * Create buy request data that can be used for working with cart items + * + * @param array $cartItemData + * @return array + */ + public function execute(array $cartItemData): array; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php index d4913ef5fd5a6..c785b632c000b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php @@ -7,10 +7,13 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\Framework\Api\AttributeInterface; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\Item; /** * Extract address fields from an Quote Address model @@ -26,19 +29,29 @@ class ExtractQuoteAddressData * @param ExtensibleDataObjectConverter $dataObjectConverter */ - /** @var Uid */ + /** + * @var Uid + */ private Uid $uidEncoder; + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + /** * @param ExtensibleDataObjectConverter $dataObjectConverter * @param Uid $uidEncoder + * @param GetAttributeValueInterface $getAttributeValue */ public function __construct( ExtensibleDataObjectConverter $dataObjectConverter, - Uid $uidEncoder + Uid $uidEncoder, + GetAttributeValueInterface $getAttributeValue ) { $this->dataObjectConverter = $dataObjectConverter; $this->uidEncoder = $uidEncoder; + $this->getAttributeValue = $getAttributeValue; } /** @@ -67,7 +80,17 @@ public function execute(QuoteAddress $address): array 'uid' => $this->uidEncoder->encode((string)$address->getAddressId()) , 'street' => $address->getStreet(), 'items_weight' => $address->getWeight(), - 'customer_notes' => $address->getCustomerNotes() + 'customer_notes' => $address->getCustomerNotes(), + 'custom_attributes' => array_map( + function (AttributeInterface $attribute) { + return $this->getAttributeValue->execute( + 'customer_address', + $attribute->getAttributeCode(), + $attribute->getValue() + ); + }, + $address->getCustomAttributes() ?? [] + ) ] ); @@ -76,7 +99,7 @@ public function execute(QuoteAddress $address): array } foreach ($address->getAllItems() as $addressItem) { - if ($addressItem instanceof \Magento\Quote\Model\Quote\Item) { + if ($addressItem instanceof Item) { $itemId = $addressItem->getItemId(); } else { $itemId = $addressItem->getQuoteItemId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php index 06fc3ad2e6657..e57deafe9c536 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php @@ -8,6 +8,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total; @@ -16,7 +17,7 @@ /** * Helper class to eliminate redundant expensive total calculations */ -class TotalsCollector +class TotalsCollector implements ResetAfterRequestInterface { /** * @var QuoteTotalsCollector @@ -34,6 +35,8 @@ class TotalsCollector private $addressTotals; /** + * TotalsCollector constructor + * * @param QuoteTotalsCollector $quoteTotalsCollector */ public function __construct(QuoteTotalsCollector $quoteTotalsCollector) @@ -43,6 +46,14 @@ public function __construct(QuoteTotalsCollector $quoteTotalsCollector) $this->addressTotals = []; } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->clearTotals(); + } + /** * Clear stored totals to force them to be recalculated the next time they're requested * @@ -101,7 +112,6 @@ public function collectAddressTotals(Quote $quote, Address $address, bool $force $this->addressTotals[$quoteId][$addressId] = $this->quoteTotalsCollector->collectAddressTotals($quote, $address); } - return $this->addressTotals[$quoteId][$addressId]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php index 85e744c026c43..b0d68aa634399 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\App\ObjectManager; /** * Category UID processor class for category uid and category id arguments @@ -23,18 +24,26 @@ class CartItemsUidArgsProcessor implements ArgumentsProcessorInterface /** @var Uid */ private $uidEncoder; + /** + * @var CustomizableOptionUidArgsProcessor + */ + private $optionUidArgsProcessor; + /** * @param Uid $uidEncoder + * @param CustomizableOptionUidArgsProcessor|null $optionUidArgsProcessor */ - public function __construct(Uid $uidEncoder) + public function __construct(Uid $uidEncoder, ?CustomizableOptionUidArgsProcessor $optionUidArgsProcessor = null) { $this->uidEncoder = $uidEncoder; + $this->optionUidArgsProcessor = + $optionUidArgsProcessor ?: ObjectManager::getInstance()->get(CustomizableOptionUidArgsProcessor::class); } /** * Process the updateCartItems arguments for cart uids * - * @param string $fieldName, + * @param string $fieldName * @param array $args * @return array * @throws GraphQlInputException @@ -58,6 +67,10 @@ public function process( $args[$filterKey]['cart_items'][$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); unset($args[$filterKey]['cart_items'][$key][self::UID]); } + if (!empty($cartItem['customizable_options'])) { + $args[$filterKey]['cart_items'][$key]['customizable_options'] = + $this->optionUidArgsProcessor->process($fieldName, $cartItem['customizable_options']); + } } } return $args; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php new file mode 100644 index 0000000000000..278239bba54fa --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CustomizableOptionUidArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'id'; + + private const UID = 'uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Process the customizable options for updateCartItems arguments for uids + * + * @param string $fieldName + * @param array $customizableOptions + * @return array + * @throws GraphQlInputException + */ + public function process(string $fieldName, array $customizableOptions): array + { + foreach ($customizableOptions as $key => $option) { + $idFilter = $option[self::ID] ?? []; + $uidFilter = $option[self::UID] ?? []; + + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'updateCartItems') { + throw new GraphQlInputException( + __( + '`%1` and `%2` can\'t be used for CustomizableOptionInput object at the same time.', + [self::ID, self::UID] + ) + ); + } elseif (!empty($uidFilter)) { + $customizableOptions[$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($customizableOptions[$key][self::UID]); + } + } + return $customizableOptions; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index dfbc20bf7abd4..4722b3db537a1 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Cart\Totals; use Magento\Quote\Model\Quote\Item; use Magento\QuoteGraphQl\Model\Cart\TotalsCollector; @@ -18,7 +19,7 @@ /** * @inheritdoc */ -class CartItemPrices implements ResolverInterface +class CartItemPrices implements ResolverInterface, ResetAfterRequestInterface { /** * @var TotalsCollector @@ -26,11 +27,13 @@ class CartItemPrices implements ResolverInterface private $totalsCollector; /** - * @var Totals + * @var Totals|null */ private $totals; /** + * CartItemPrices constructor + * * @param TotalsCollector $totalsCollector */ public function __construct( @@ -39,6 +42,14 @@ public function __construct( $this->totalsCollector = $totalsCollector; } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->totals = null; + } + /** * @inheritdoc */ @@ -49,14 +60,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } /** @var Item $cartItem */ $cartItem = $value['model']; - if (!$this->totals) { // The totals calculation is based on quote address. // But the totals should be calculated even if no address is set $this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote()); } $currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode(); - return [ 'model' => $cartItem, 'price' => [ @@ -88,13 +97,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * * @param Item $cartItem * @param string $currencyCode - * @return array + * @return array|null */ private function getDiscountValues($cartItem, $currencyCode) { $itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts(); if ($itemDiscounts) { - $discountValues=[]; + $discountValues = []; foreach ($itemDiscounts as $value) { $discount = []; $amount = []; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php index d0c69b8b54497..1f7e0f914d7c2 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php @@ -18,6 +18,8 @@ */ class Discounts implements ResolverInterface { + public const TYPE_SHIPPING = "SHIPPING"; + public const TYPE_ITEM = "ITEM"; /** * @inheritdoc */ @@ -41,21 +43,22 @@ private function getDiscountValues(Quote $quote) { $discountValues=[]; $address = $quote->getShippingAddress(); - $totals = $address->getTotals(); - if ($totals && is_array($totals)) { - foreach ($totals as $total) { - if (stripos($total->getCode(), 'total') === false && $total->getValue() < 0.00) { - $discount = []; - $amount = []; - $discount['label'] = $total->getTitle() ?: __('Discount'); - $amount['value'] = $total->getValue() * -1; - $amount['currency'] = $quote->getQuoteCurrencyCode(); - $discount['amount'] = $amount; - $discountValues[] = $discount; - } + $totalDiscounts = $address->getExtensionAttributes()->getDiscounts(); + + if ($totalDiscounts && is_array($totalDiscounts)) { + foreach ($totalDiscounts as $value) { + $discount = []; + $amount = []; + $discount['label'] = $value->getRuleLabel() ?: __('Discount'); + /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discountData */ + $discountData = $value->getDiscountData(); + $discount['applied_to'] = $discountData->getAppliedTo(); + $amount['value'] = $discountData->getAmount(); + $amount['currency'] = $quote->getQuoteCurrencyCode(); + $discount['amount'] = $amount; + $discountValues[] = $discount; } - return $discountValues; } - return null; + return $discountValues ?: null; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php index abd5ceca881f4..307087391b89d 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -87,7 +87,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); /** Check if the current user is allowed to perform actions with the cart */ - $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { $this->cartItemRepository->deleteById($cartId, $itemId); @@ -97,7 +97,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__($e->getMessage()), $e); } - $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php index b20bbe0e00660..01e9a95dd5c76 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -50,11 +50,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return null; } - list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); - /** @var Rate $rate */ + $carrierCode = $methodCode = null; foreach ($rates as $rate) { if ($rate->getCode() === $address->getShippingMethod()) { + $carrierCode = $rate->getCarrier(); + $methodCode = $rate->getMethod(); break; } } diff --git a/app/code/Magento/QuoteGraphQl/README.md b/app/code/Magento/QuoteGraphQl/README.md index 396f886fc04be..7eebc7c5e5291 100644 --- a/app/code/Magento/QuoteGraphQl/README.md +++ b/app/code/Magento/QuoteGraphQl/README.md @@ -6,6 +6,7 @@ to generate quote (cart) information endpoints. Also provides endpoints for modi ## Installation Before installing this module, note that the Magento_QuoteGraphQl is dependent on the following modules: + - `Magento_CatalogGraphQl` This module does not introduce any database schema modifications or new data. @@ -20,7 +21,7 @@ Extension developers can interact with the Magento_QuoteDownloadableLinks module ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php new file mode 100644 index 0000000000000..68f52c4a348d9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Test\Unit\Model\Resolver\ShippingAddress; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\Context; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Resolver\ShippingAddress\SelectedShippingMethod; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Quote\Model\Cart\ShippingMethodConverter; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Address\Rate; + +/** + * @see SelectedShippingMethod + */ +class SelectedShippingMethodTest extends TestCase +{ + /** + * @var SelectedShippingMethod + */ + private $selectedShippingMethod; + + /** + * @var ShippingMethodConverter|MockObject + */ + private $shippingMethodConverterMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var Address|MockObject + */ + private $addressMock; + + /** + * @var Rate|MockObject + */ + private $rateMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var Quote|MockObject + */ + private $quoteMock; + + /** + * @var array + */ + private $valueMock = []; + + protected function setUp(): void + { + $this->shippingMethodConverterMock = $this->createMock(ShippingMethodConverter::class); + $this->contextMock = $this->createMock(Context::class); + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->addressMock = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->onlyMethods(['getShippingMethod','getAllShippingRates','getQuote',]) + ->AddMethods(['getShippingAmount','getMethod',]) + ->getMock(); + $this->rateMock = $this->getMockBuilder(Rate::class) + ->disableOriginalConstructor() + ->AddMethods(['getCode','getCarrier','getMethod']) + ->getMock(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->addMethods([ + 'getQuoteCurrencyCode', + 'getMethodTitle', + 'getCarrierTitle', + 'getPriceExclTax', + 'getPriceInclTax' + ]) + ->getMock(); + $this->selectedShippingMethod = new SelectedShippingMethod( + $this->shippingMethodConverterMock + ); + } + + public function testResolveWithoutModelInValueParameter(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('"model" value should be specified'); + $this->selectedShippingMethod->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock, + $this->valueMock + ); + } + + public function testResolve(): void + { + $this->valueMock = ['model' => $this->addressMock]; + $this->quoteMock + ->method('getQuoteCurrencyCode') + ->willReturn('USD'); + $this->quoteMock + ->method('getMethodTitle') + ->willReturn('method_title'); + $this->quoteMock + ->method('getCarrierTitle') + ->willReturn('carrier_title'); + $this->quoteMock + ->expects($this->once()) + ->method('getPriceExclTax') + ->willReturn('PriceExclTax'); + $this->quoteMock + ->expects($this->once()) + ->method('getPriceInclTax') + ->willReturn('PriceInclTax'); + $this->rateMock + ->expects($this->once()) + ->method('getCode') + ->willReturn('shipping_method'); + $this->rateMock + ->expects($this->once()) + ->method('getCarrier') + ->willReturn('shipping_carrier'); + $this->rateMock + ->expects($this->once()) + ->method('getMethod') + ->willReturn('shipping_carrier'); + $this->addressMock + ->method('getAllShippingRates') + ->willReturn([$this->rateMock]); + $this->addressMock + ->method('getShippingMethod') + ->willReturn('shipping_method'); + $this->addressMock + ->method('getShippingAmount') + ->willReturn('shipping_amount'); + $this->addressMock + ->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->shippingMethodConverterMock->method('modelToDataObject') + ->willReturn($this->quoteMock); + $this->selectedShippingMethod->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock, + $this->valueMock + ); + } +} diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 24cb1382634c2..62c37801cbcbb 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -15,7 +15,8 @@ "magento/module-directory": "*", "magento/module-graph-ql": "*", "magento/module-gift-message": "*", - "magento/module-catalog-inventory": "*" + "magento/module-catalog-inventory": "*", + "magento/module-eav-graph-ql": "*" }, "suggest": { "magento/module-graph-ql-cache": "*", diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 27433a30f3c92..1b65bdcf4564c 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -62,7 +62,8 @@ input CartItemInput @doc(description: "Defines an item to be added to the cart." } input CustomizableOptionInput @doc(description: "Defines a customizable option.") { - id: Int @doc(description: "The customizable option ID of the product.") + uid: ID @doc(description: "The unique ID for a `CartItemInterface` object.") + id: Int @deprecated(reason: "Use `uid` instead.") @doc(description: "The customizable option ID of the product.") value_string: String! @doc(description: "The string value of the option.") } @@ -125,6 +126,10 @@ input CartAddressInput @doc(description: "Defines the billing or shipping addres telephone: String @doc(description: "The telephone number for the billing or shipping address.") vat_id: String @doc(description: "The VAT company number for billing or shipping address.") save_in_address_book: Boolean @doc(description: "Determines whether to save the address in the customer's address book. The default value is true.") + fax: String @doc(description: "The customer's fax number.") + middlename: String @doc(description: "The middle name of the person associated with the billing/shipping 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.") } input SetShippingMethodsOnCartInput @doc(description: "Applies one or shipping methods to the cart.") { @@ -232,6 +237,10 @@ interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Mo country: CartAddressCountry! @doc(description: "An object containing the country label and code.") telephone: String @doc(description: "The telephone number for the billing or shipping address.") vat_id: String @doc(description: "The VAT company number for billing or shipping address.") + fax: String @doc(description: "The customer's fax number.") + middlename: String @doc(description: "The middle name of the person associated with the billing/shipping 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.") } type ShippingCartAddress implements CartAddressInterface @doc(description: "Contains shipping addresses and methods.") { @@ -358,11 +367,17 @@ enum CartItemErrorType { ITEM_INCREMENTS } -type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") { +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item, shipping.") { amount: Money! @doc(description:"The amount of the discount.") + applied_to: CartDiscountType! @doc(description:"The type of the entity the discount is applied to.") label: String! @doc(description:"A description of the discount.") } +enum CartDiscountType { + ITEM + SHIPPING +} + type CartItemPrices @doc(description: "Contains details about the price of the item, including taxes and discounts.") { price: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.") price_including_tax: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.") diff --git a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php index e5084d4c9f9b6..1dec3387c87f6 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php @@ -118,6 +118,9 @@ public function getRelations(array $products, int $linkType): array $collection = $link->getLinkCollection(); $collection->addFieldToFilter('product_id', ['in' => array_keys($productsByActualIds)]); $collection->addLinkTypeIdFilter(); + $collection->joinAttributes(); + $collection->addOrder('product_id'); + $collection->addOrder('position', 'asc'); //Prepare map $map = []; diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index fac7b23d408e3..f35af6f4885d2 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -94,9 +94,7 @@ private function findRelations(array $products, array $loadAttributes, int $link $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); $relatedSearchResult = $this->productDataProvider->getList( $this->searchCriteriaBuilder->create(), - $loadAttributes, - false, - true + $loadAttributes ); //Filling related products map. /** @var \Magento\Catalog\Api\Data\ProductInterface[] $relatedProducts */ diff --git a/app/code/Magento/RelatedProductGraphQl/README.md b/app/code/Magento/RelatedProductGraphQl/README.md index 7aa93403a6949..d25286e686aed 100644 --- a/app/code/Magento/RelatedProductGraphQl/README.md +++ b/app/code/Magento/RelatedProductGraphQl/README.md @@ -16,4 +16,4 @@ Extension developers can interact with the Magento_QuoteDownloadableLinks module ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md index 060aeb24f473a..c6e55ab091bc3 100644 --- a/app/code/Magento/ReleaseNotification/README.md +++ b/app/code/Magento/ReleaseNotification/README.md @@ -19,6 +19,7 @@ Extension developers can interact with the Magento_ReleaseNotification module. F ### UI components You can extend release notification updates using the configuration files located in the `view/adminhtml/ui_component` directory: + - `release_notification` For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). @@ -27,19 +28,19 @@ For information about a UI component in Magento 2, see [Overview of UI component ### Purpose and Content -* Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release. -* Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version. -* The modal is enabled with pagination functionality to allow for easy navigation between each modal page. -* Each modal page includes detailed information about a highlighted feature of the Magento release or other notification. -* Release Notification modal content is determined and provided by Magento Marketing. +- Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release. +- Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version. +- The modal is enabled with pagination functionality to allow for easy navigation between each modal page. +- Each modal page includes detailed information about a highlighted feature of the Magento release or other notification. +- Release Notification modal content is determined and provided by Magento Marketing. ### Content Retrieval Release notification content is maintained by Magento for each Magento version, edition, and locale. To retrieve the content, a response is returned from a request with the following parameters: -* **version** = The Magento version that the client has installed (ex. 2.4.0). -* **edition** = The Magento edition that the client has installed (ex. Community). -* **locale** = The chosen locale of the admin user (ex. en_US). +- **version** = The Magento version that the client has installed (ex. 2.4.0). +- **edition** = The Magento edition that the client has installed (ex. Community). +- **locale** = The chosen locale of the admin user (ex. en_US). The module will make three attempts to retrieve content for the parameters in the order listed: @@ -51,21 +52,21 @@ If there is no content to be retrieved after these requests, the release notific ### Content Guidelines -The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification. +The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification. Each modal page can have the following optional content: -* Main Content - * Title - * URL to the image to be displayed alongside the title - * Text body - * Bullet point list -* Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed - * Sub heading title - * URL to the image to be display before the sub heading title - * Sub heading content -* Footer - * Footer content text +- Main Content + - Title + - URL to the image to be displayed alongside the title + - Text body + - Bullet point list +- Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed + - Sub heading title + - URL to the image to be display before the sub heading title + - Sub heading content +- Footer + - Footer content text The Sub Heading section is ideally used on the first modal page as a way to describe one to three highlighted features that will be presented in greater detail on the following modal pages. It is recommended to use the Main Content -> Text Body and Bullet Point lists as the paragraph and list content displayed on a highlighted feature's detail modal page. diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv index 178482dc7a980..50a4587b4398c 100644 --- a/app/code/Magento/ReleaseNotification/i18n/en_US.csv +++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv @@ -5,11 +5,11 @@ "<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""https://devdocs.magento.com/magento-release-information.html"" + <a href=""hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>","<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""https://devdocs.magento.com/magento-release-information.html"" + <a href=""hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>" diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml index 9c6d152bed27b..16b7b94da858b 100644 --- a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml +++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml @@ -67,7 +67,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -127,7 +127,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -208,7 +208,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -289,7 +289,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index e1fda91923e4c..f67eee4ddb0c5 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -87,4 +87,15 @@ public function getDriver($code = self::REMOTE): DriverInterface return parent::getDriver($code); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index 4593c26281554..ae0dc791c275e 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -129,4 +129,15 @@ public function getDirectoryCodes(): array { return $this->directoryCodes; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/app/code/Magento/Reports/README.md b/app/code/Magento/Reports/README.md index 1fac1a15782cb..3d90323a3ee5d 100644 --- a/app/code/Magento/Reports/README.md +++ b/app/code/Magento/Reports/README.md @@ -1,4 +1,5 @@ Magento_Reports module provides ability to collect various reports such as: + - products reports (bestsellers, low stock, most viewed, products ordered), - sales reports (orders, tax, invoiced, shipping, refunds, coupons, and PayPal settlement reports), - customer reports (new accounts, customer by order totals, customers by number of orders), diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml index 600291dffade4..74296fbe66b38 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95960"/> <useCaseId value="MAGETWO-95823"/> + <group value="cloud"/> </annotations> <before> @@ -75,7 +76,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> <actionGroup ref="AdminGoToOrdersReportPageActionGroup" stepKey="goToOrdersReportPage1"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml index 88c94a27a83fb..f550bf6d58b1f 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml index efe988acbcf7d..d4ae4752e93a4 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml index 14db012e76888..c753ff49a0ffb 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14163"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml index d6a3ae8bcd201..261ff3c9e5377 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml index e9ed4caa7ef03..c99c6faf7c1e6 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14162"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml index 7756a43c68ace..cad8185fe204e 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml @@ -16,6 +16,7 @@ <description value="A product must don't presents on 'Low Stock' report if the product is disabled."/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml index 8d8ceb69ba7ba..d2de9cefd4f55 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml index 30d9392071a9c..5d4e5c494f7a4 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml index cb17169c4cf8c..30980009d847b 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml index c3d0f3b51f69e..313770e7411dc 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml index fb44eca668e68..8cad119c65578 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml @@ -52,7 +52,7 @@ </after> <!--Add first configurable product to order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToFirstOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToFirstOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addFirstConfigurableProductToOrder"> @@ -63,7 +63,7 @@ <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitFirstOrder" /> <!--Add second configurable product to order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToSecondOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToSecondOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addSecondConfigurableProductToOrder"> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml index d42a41a45d6f8..51c3148bcb487 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml index 388be27113074..6595baa826ebc 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml index ec8b60ac743fd..601c2015ea7e7 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml index 9a32e20594dfa..3d0a247a0b180 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml index f8091d4f63101..65fb97d60fa77 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14161"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml index d68eb332d81a3..45ba0e76f67a5 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php index 50398d42a7019..f2d6433ec24bb 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php @@ -278,20 +278,17 @@ public function testPrepareSummary($useAggregatedData, $mainTable, $isFilter, $g * @param int $range * @param string $customStart * @param string $customEnd - * @param string $expectedInterval + * @param array $expectedInterval * * @return void * @dataProvider firstPartDateRangeDataProvider */ public function testGetDateRangeFirstPart($range, $customStart, $customEnd, $expectedInterval): void { - $timeZoneToReturn = date_default_timezone_get(); - date_default_timezone_set('UTC'); $result = $this->collection->getDateRange($range, $customStart, $customEnd); $interval = $result['to']->diff($result['from']); - date_default_timezone_set($timeZoneToReturn); $intervalResult = $interval->format('%y %m %d %h:%i:%s'); - $this->assertEquals($expectedInterval, $intervalResult); + $this->assertContains($intervalResult, $expectedInterval); } /** @@ -464,9 +461,9 @@ public function useAggregatedDataDataProvider(): array public function firstPartDateRangeDataProvider(): array { return [ - ['', '', '', '0 0 0 23:59:59'], - ['24h', '', '', '0 0 1 0:0:0'], - ['7d', '', '', '0 0 6 23:59:59'] + ['', '', '', ['0 0 0 23:59:59', '0 0 1 0:59:59', '0 0 0 22:59:59']], + ['24h', '', '', ['0 0 1 0:0:0', '0 0 1 1:0:0', '0 0 0 23:0:0']], + ['7d', '', '', ['0 0 6 23:59:59', '0 0 7 0:59:59', '0 0 6 22:59:59']] ]; } diff --git a/app/code/Magento/RequireJs/README.md b/app/code/Magento/RequireJs/README.md index 8ed9f88095606..55573d9c5fafd 100644 --- a/app/code/Magento/RequireJs/README.md +++ b/app/code/Magento/RequireJs/README.md @@ -1,12 +1,15 @@ # Overview + ## Purpose of module The Magento\RequireJs module introduces support for RequireJs JavaScript library and provides infrastructure for other modules to have them declared related configuration for RequireJs library. # Deployment + ## System requirements The Magento\RequireJs module does not have any specific system requirements. ## Install + The Magento\RequireJs module is installed automatically (using the native Magento Setup). No additional actions required. diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 1fb7e7df2461f..900cdc1f330d4 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -21,22 +21,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { /** - * Entities alias - * * @var array */ protected $_entitiesAlias = []; /** - * Review store table - * * @var string */ protected $_reviewStoreTable; /** - * Add store data flag - * * @var bool */ protected $_addStoreDataFlag = false; @@ -159,6 +153,17 @@ protected function _construct() $this->_initTables(); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_entitiesAlias = []; + $this->_addStoreDataFlag = false; + $this->_storesIds = []; + } + /** * Initialize select * diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml index 57e7d44dab10d..4339d423159f1 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml @@ -15,6 +15,7 @@ <description value="Admin able see navigate head menu Marketing is active, when open page Marketing > Pending Reviews"/> <severity value="MAJOR"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml index 32f11b08616cb..218899f1f6889 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml index 18b45155fdc67..bca91c2ed88fd 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="review"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml index 51b4ff58e88f1..475349e6d19a5 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml index e577289ed3679..c4ee11c9b8d21 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml index 49e574c09fe78..decb69b6a7027 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml index 1209bd4d351e0..4b49d3d0079d2 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-39737"/> <testCaseId value="MC-39838"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Step1. Login as admin--> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml index e9a08a3e196f5..5c52335c81796 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-21818"/> <group value="review"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set general/single_store_mode/enabled 0" stepKey="enabledSingleStoreMode"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml index b577c415fd242..03b26005459af 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml @@ -16,6 +16,7 @@ <description value="Verify no javascript error occurs when customer clicks 'Add Your Review' link"/> <severity value="MAJOR"/> <group value="review"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml index c581fd2757ad3..ce8b68a0e4af9 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml @@ -15,6 +15,7 @@ <title value="Product Review is Available in Customer's Account"/> <description value="Customer should be able see product review on My Product Reviews page in Customer account"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index b714bac3a7ab3..aadcef81da880 100644 --- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml @@ -26,6 +26,7 @@ <arguments> <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> </arguments> + <container name="form.additional.review.info" as="form_additional_review_info"/> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before"/> </block> </block> diff --git a/app/code/Magento/Review/view/frontend/templates/form.phtml b/app/code/Magento/Review/view/frontend/templates/form.phtml index 1a01bfd387cde..17dbde65bf7e6 100644 --- a/app/code/Magento/Review/view/frontend/templates/form.phtml +++ b/app/code/Magento/Review/view/frontend/templates/form.phtml @@ -72,6 +72,9 @@ </div> </div> </fieldset> + <fieldset class="fieldset additional_info"> + <?= $block->getChildHtml('form_additional_review_info') ?> + </fieldset> <div class="actions-toolbar review-form-actions"> <div class="primary actions-primary"> <button type="submit" class="action submit primary" diff --git a/app/code/Magento/Robots/README.md b/app/code/Magento/Robots/README.md index 936dbe973a3ee..c2fe027715f67 100644 --- a/app/code/Magento/Robots/README.md +++ b/app/code/Magento/Robots/README.md @@ -1,3 +1,4 @@ -The Robots module provides the following functionalities: +The Robots module provides the following functionalities: + * contains a router to match application action class for requests to the `robots.txt` file; * allows obtaining the content of the `robots.txt` file depending on the settings of the current website. diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php index e19d56dd46f57..b9312a8b407fd 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php @@ -174,7 +174,6 @@ public function testValidateEmptyEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') ->willReturn($newResource); @@ -190,7 +189,6 @@ public function testValidateEmptyEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->setResource($newResource); $this->assertFalse($this->_condition->validate($product)); @@ -228,7 +226,6 @@ public function testValidateSetEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') @@ -277,7 +274,6 @@ public function testValidateSetEntityAttributeValuesWithoutResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') @@ -303,7 +299,6 @@ public function testValidateSetEntityAttributeValuesWithoutResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->setResource($newResource); $product->setId(1); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index acfb654dca110..4e47343c3d994 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -40,9 +40,9 @@ public function execute() try { $data = $this->getRequest()->getPost('history'); if (empty($data['comment']) && $data['status'] == $order->getDataByKey('status')) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The comment is missing. Enter and try again.') - ); + $error = 'Please provide a comment text or ' . + 'update the order status to be able to submit a comment for this order.'; + throw new \Magento\Framework\Exception\LocalizedException(__($error)); } $orderStatus = $this->getOrderStatus($order->getDataByKey('status'), $data['status']); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php index 65ccb43879ac6..643ed5445231f 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php @@ -3,18 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\ForwardFactory; -use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\RegexValidator; +use Magento\Framework\View\Result\PageFactory; use Magento\Sales\Controller\Adminhtml\Order\Create as CreateAction; use Magento\Store\Model\StoreManagerInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LoadBlock extends CreateAction implements HttpPostActionInterface, HttpGetActionInterface { /** @@ -28,13 +36,19 @@ class LoadBlock extends CreateAction implements HttpPostActionInterface, HttpGet private $storeManager; /** - * @param Action\Context $context - * @param \Magento\Catalog\Helper\Product $productHelper - * @param \Magento\Framework\Escaper $escaper + * @var RegexValidator + */ + private RegexValidator $regexValidator; + + /** + * @param Context $context + * @param Product $productHelper + * @param Escaper $escaper * @param PageFactory $resultPageFactory * @param ForwardFactory $resultForwardFactory * @param RawFactory $resultRawFactory * @param StoreManagerInterface|null $storeManager + * @param RegexValidator|null $regexValidator */ public function __construct( Action\Context $context, @@ -43,7 +57,8 @@ public function __construct( PageFactory $resultPageFactory, ForwardFactory $resultForwardFactory, RawFactory $resultRawFactory, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + RegexValidator $regexValidator = null ) { $this->resultRawFactory = $resultRawFactory; parent::__construct( @@ -55,6 +70,8 @@ public function __construct( ); $this->storeManager = $storeManager ?: ObjectManager::getInstance() ->get(StoreManagerInterface::class); + $this->regexValidator = $regexValidator + ?: ObjectManager::getInstance()->get(RegexValidator::class); } /** @@ -64,6 +81,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws LocalizedException */ public function execute() { @@ -84,6 +102,12 @@ public function execute() $asJson = $request->getParam('json'); $block = $request->getParam('block'); + if ($block && !$this->regexValidator->validateParamRegex($block)) { + throw new LocalizedException( + __('The url has invalid characters. Please correct and try again.') + ); + } + /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); if ($asJson) { diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index a2aaed18cb56e..15d853cabfbee 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -29,6 +29,8 @@ class DownloadCustomOption extends \Magento\Framework\App\Action\Action implemen /** * @var \Magento\Framework\Unserialize\Unserialize * @deprecated 101.0.0 + * @deprecated No longer used + * @see $serializer */ protected $unserialize; @@ -106,7 +108,7 @@ public function execute() if ($this->getRequest()->getParam('key') != $info['secret_key']) { return $resultForward->forward('noroute'); } - $this->download->downloadFile($info); + return $this->download->createResponse($info); } catch (\Exception $e) { return $resultForward->forward('noroute'); } diff --git a/app/code/Magento/Sales/Helper/Admin.php b/app/code/Magento/Sales/Helper/Admin.php index 0e0d8213cb791..1e2e5dfb79668 100644 --- a/app/code/Magento/Sales/Helper/Admin.php +++ b/app/code/Magento/Sales/Helper/Admin.php @@ -166,7 +166,13 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) $internalErrors = libxml_use_internal_errors(true); - $data = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); + $convmap = [0x80, 0x10FFFF, 0, 0x1FFFFF]; + $data = mb_encode_numericentity( + $data, + $convmap, + 'UTF-8' + ); + $domDocument->loadHTML( '<html><body id="' . $wrapperElementId . '">' . $data . '</body></html>' ); @@ -192,7 +198,17 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) } } - $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); + $result = mb_decode_numericentity( + // phpcs:ignore Magento2.Functions.DiscouragedFunction + html_entity_decode( + $domDocument->saveHTML(), + ENT_QUOTES|ENT_SUBSTITUTE, + 'UTF-8' + ), + $convmap, + 'UTF-8' + ); + preg_match('/<body id="' . $wrapperElementId . '">(.+)<\/body><\/html>$/si', $result, $matches); $data = !empty($matches) ? $matches[1] : ''; } diff --git a/app/code/Magento/Sales/Model/Download.php b/app/code/Magento/Sales/Model/Download.php index e4a0a0ba93e7b..8f7b991f3ce4c 100644 --- a/app/code/Magento/Sales/Model/Download.php +++ b/app/code/Magento/Sales/Model/Download.php @@ -67,8 +67,22 @@ public function __construct( * @param array $info * @return void * @throws \Exception + * @deprecated No longer recommended + * @see createResponse() */ public function downloadFile($info) + { + $this->createResponse($info); + } + + /** + * Returns a file response + * + * @param array $info + * @return \Magento\Framework\App\ResponseInterface + * @throws \Exception + */ + public function createResponse($info) { $relativePath = $info['order_path']; if (!$this->_isCanProcessed($relativePath)) { @@ -80,7 +94,7 @@ public function downloadFile($info) ); } } - $this->_fileFactory->create( + return $this->_fileFactory->create( $info['title'], ['value' => $this->_rootDir->getRelativePath($relativePath), 'type' => 'filename'], $this->rootDirBasePath, diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index f272a4638a170..bda17880dc8cd 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Model; +use Magento\Catalog\Model\Product\Type; use Magento\Config\Model\Config\Source\Nooptreq; use Magento\Directory\Model\Currency; use Magento\Directory\Model\RegionFactory; @@ -197,7 +198,8 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface /** * @var \Magento\Catalog\Api\ProductRepositoryInterface - * @deprecated 100.1.0 Remove unused dependency. + * @deprecated 100.1.0 + * @see Remove unused dependency */ protected $productRepository; @@ -831,7 +833,10 @@ public function canShip() } foreach ($this->getAllItems() as $item) { - if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && + $qtyToShip = !$item->getParentItem() || $item->getParentItem()->getProductType() !== Type::TYPE_BUNDLE ? + $item->getQtyToShip() : $item->getSimpleQtyToShip(); + + if ($qtyToShip > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip() && !$this->isRefunded($item)) { return true; } @@ -1741,7 +1746,17 @@ public function getStatusHistoryById($statusId) public function addStatusHistory(\Magento\Sales\Model\Order\Status\History $history) { $history->setOrder($this); - $this->setStatus($history->getStatus()); + if (!$history->getStatus()) { + $previousStatus = $this->getStatusHistoryCollection()->getFirstItem()->getData('status'); + if (!$previousStatus) { + $defaultStatus = $this->getConfig()->getStateDefaultStatus($this->getState()); + $history->setStatus($defaultStatus); + } else { + $history->setStatus($previousStatus); + } + } else { + $this->setStatus($history->getStatus()); + } if (!$history->getId()) { $this->setStatusHistories(array_merge($this->getStatusHistories(), [$history])); $this->setDataChanges(true); diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index 3ef0c99bb2b05..e08ed2e2814a3 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -134,8 +134,8 @@ public function collect(Creditmemo $creditmemo) $baseShippingDiscountTaxCompensationAmount = 0; $shippingDelta = $baseOrderShippingAmount - $baseOrderShippingRefundedAmount; - if ($shippingDelta > $creditmemo->getBaseShippingAmount() || - $this->isShippingIncludeTaxWithTaxAfterDiscount($order->getStoreId())) { + if ($orderShippingAmount > 0 && ($shippingDelta > $creditmemo->getBaseShippingAmount() || + $this->isShippingIncludeTaxWithTaxAfterDiscount($order->getStoreId()))) { $part = $creditmemo->getShippingAmount() / $orderShippingAmount; $basePart = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $order->getShippingTaxAmount() * $part; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 81616520c077a..905f7f89697e5 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Pdf\Image; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Sales\Model\RtlTextHandler; use Magento\Store\Model\ScopeInterface; @@ -61,6 +62,11 @@ abstract class AbstractPdf extends \Magento\Framework\DataObject */ private $rtlTextHandler; + /** + * @var \Magento\Framework\File\Pdf\Image + */ + private $image; + /** * Retrieve PDF * @@ -149,6 +155,7 @@ abstract public function getPdf(); * @param array $data * @param Database $fileStorageDatabase * @param RtlTextHandler|null $rtlTextHandler + * @param Image $image * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -164,7 +171,8 @@ public function __construct( \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, array $data = [], Database $fileStorageDatabase = null, - ?RtlTextHandler $rtlTextHandler = null + ?RtlTextHandler $rtlTextHandler = null, + ?Image $image = null ) { $this->addressRenderer = $addressRenderer; $this->_paymentData = $paymentData; @@ -179,6 +187,7 @@ public function __construct( $this->inlineTranslation = $inlineTranslation; $this->fileStorageDatabase = $fileStorageDatabase ?: ObjectManager::getInstance()->get(Database::class); $this->rtlTextHandler = $rtlTextHandler ?: ObjectManager::getInstance()->get(RtlTextHandler::class); + $this->image = $image ?: ObjectManager::getInstance()->get(Image::class); parent::__construct($data); } @@ -279,7 +288,7 @@ protected function insertLogo(&$page, $store = null) $this->fileStorageDatabase->saveFileToFilesystem($imagePath); } if ($this->_mediaDirectory->isFile($imagePath)) { - $image = \Zend_Pdf_Image::imageWithPath($this->_mediaDirectory->getAbsolutePath($imagePath)); + $image = $this->image->imageWithPathAdvanced($this->_mediaDirectory->getAbsolutePath($imagePath)); $top = 830; //top border of the page $widthLimit = 270; @@ -522,7 +531,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if (!$order->getIsVirtual()) { $this->y = $addressesStartY; - $shippingAddress = $shippingAddress ?? []; + $shippingAddress = $shippingAddress ?? []; // @phpstan-ignore-line foreach ($shippingAddress as $value) { if ($value !== '') { $text = []; diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index ef9c6fc628dd5..faff61d15fc3a 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -10,6 +10,7 @@ use Magento\Sales\Model\AbstractModel; use Magento\Sales\Model\EntityInterface; use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\Collection as CommentsCollection; +use Magento\Sales\Model\ValidatorInterface; /** * Sales order shipment model @@ -26,26 +27,26 @@ */ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterface { - const STATUS_NEW = 1; + public const STATUS_NEW = 1; - const REPORT_DATE_TYPE_ORDER_CREATED = 'order_created'; + public const REPORT_DATE_TYPE_ORDER_CREATED = 'order_created'; - const REPORT_DATE_TYPE_SHIPMENT_CREATED = 'shipment_created'; + public const REPORT_DATE_TYPE_SHIPMENT_CREATED = 'shipment_created'; /** * Store address */ - const XML_PATH_STORE_ADDRESS1 = 'shipping/origin/street_line1'; + public const XML_PATH_STORE_ADDRESS1 = 'shipping/origin/street_line1'; - const XML_PATH_STORE_ADDRESS2 = 'shipping/origin/street_line2'; + public const XML_PATH_STORE_ADDRESS2 = 'shipping/origin/street_line2'; - const XML_PATH_STORE_CITY = 'shipping/origin/city'; + public const XML_PATH_STORE_CITY = 'shipping/origin/city'; - const XML_PATH_STORE_REGION_ID = 'shipping/origin/region_id'; + public const XML_PATH_STORE_REGION_ID = 'shipping/origin/region_id'; - const XML_PATH_STORE_ZIP = 'shipping/origin/postcode'; + public const XML_PATH_STORE_ZIP = 'shipping/origin/postcode'; - const XML_PATH_STORE_COUNTRY_ID = 'shipping/origin/country_id'; + public const XML_PATH_STORE_COUNTRY_ID = 'shipping/origin/country_id'; /** * Order entity type @@ -104,6 +105,11 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa */ private $commentsCollection; + /** + * @var ValidatorInterface|null + */ + private $validator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -117,6 +123,7 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ValidatorInterface|null $validator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -131,13 +138,15 @@ public function __construct( \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ?ValidatorInterface $validator = null ) { $this->_shipmentItemCollectionFactory = $shipmentItemCollectionFactory; $this->_trackCollectionFactory = $trackCollectionFactory; $this->_commentFactory = $commentFactory; $this->_commentCollectionFactory = $commentCollectionFactory; $this->orderRepository = $orderRepository; + $this->validator = $validator; parent::__construct( $context, $registry, @@ -159,6 +168,14 @@ protected function _construct() $this->_init(\Magento\Sales\Model\ResourceModel\Order\Shipment::class); } + /** + * @inheritDoc + */ + protected function _getValidationRulesBeforeSave(): ?ValidatorInterface + { + return $this->validator; + } + /** * Load shipment by increment id * diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Item.php b/app/code/Magento/Sales/Model/Order/Shipment/Item.php index 0da936e74ca6c..660b6acfba8f7 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Item.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Item.php @@ -278,7 +278,7 @@ public function getWeight() } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -286,7 +286,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotal($amount) { @@ -294,7 +294,7 @@ public function setRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrice($price) { @@ -302,7 +302,7 @@ public function setPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -310,7 +310,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($id) { @@ -318,7 +318,7 @@ public function setProductId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($id) { @@ -326,7 +326,7 @@ public function setOrderItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -334,7 +334,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -342,7 +342,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setName($name) { @@ -350,7 +350,7 @@ public function setName($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSku($sku) { @@ -358,7 +358,7 @@ public function setSku($sku) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\ShipmentItemExtensionInterface|null */ @@ -368,7 +368,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\ShipmentItemExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php index ad73b22e94555..cf72a0f1b93c4 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php @@ -44,7 +44,7 @@ class ShipmentRepository implements \Magento\Sales\Api\ShipmentRepositoryInterfa /** * @param Metadata $metadata * @param SearchResultFactory $searchResultFactory - * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( Metadata $metadata, @@ -53,7 +53,9 @@ public function __construct( ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; - $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->collectionProcessor = $collectionProcessor ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class + ); } /** @@ -146,6 +148,8 @@ public function save(\Magento\Sales\Api\Data\ShipmentInterface $entity) try { $this->metadata->getMapper()->save($entity); $this->registry[$entity->getEntityId()] = $entity; + } catch (\Magento\Framework\Validator\Exception $exception) { + throw new CouldNotSaveException(__($exception->getMessage()), $exception); } catch (\Exception $e) { throw new CouldNotSaveException(__("The shipment couldn't be saved."), $e); } @@ -162,20 +166,4 @@ public function create() { return $this->metadata->getNewInstance(); } - - /** - * Retrieve collection processor - * - * @deprecated 101.0.0 - * @return CollectionProcessorInterface - */ - private function getCollectionProcessor() - { - if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - } - return $this->collectionProcessor; - } } diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index 6ad8d73b1fc4d..45946697ba891 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -12,6 +12,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Sales\Api\Data\OrderExtensionFactory; use Magento\Sales\Api\Data\OrderExtensionInterface; use Magento\Sales\Api\Data\OrderInterface; @@ -28,8 +29,9 @@ * Repository class * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure */ -class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface +class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface, ResetAfterRequestInterface { /** * @var Metadata @@ -347,4 +349,12 @@ protected function addFilterGroupToCollection( $searchResult->addFieldToFilter($fields, $conditions); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->registry = []; + } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php index e37e8ab843e73..91c3f2fd1cf2a 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php @@ -5,25 +5,39 @@ */ namespace Magento\Sales\Model\ResourceModel\Report; +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\Timezone\Validator; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Reports\Model\FlagFactory; +use Magento\Sales\Model\ResourceModel\Helper; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; + /** * Bestsellers report resource model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Bestsellers extends AbstractReport { - const AGGREGATION_DAILY = 'daily'; + public const AGGREGATION_DAILY = 'daily'; - const AGGREGATION_MONTHLY = 'monthly'; + public const AGGREGATION_MONTHLY = 'monthly'; - const AGGREGATION_YEARLY = 'yearly'; + public const AGGREGATION_YEARLY = 'yearly'; /** - * @var \Magento\Catalog\Model\ResourceModel\Product + * @var Product */ protected $_productResource; /** - * @var \Magento\Sales\Model\ResourceModel\Helper + * @var Helper */ protected $_salesResourceHelper; @@ -37,29 +51,36 @@ class Bestsellers extends AbstractReport ]; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Reports\Model\FlagFactory $reportsFlagFactory - * @param \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\Catalog\Model\ResourceModel\Product $productResource - * @param \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper + * @var StoreManagerInterface + */ + protected $storeManager; + + /** + * @param Context $context + * @param LoggerInterface $logger + * @param TimezoneInterface $localeDate + * @param FlagFactory $reportsFlagFactory + * @param Validator $timezoneValidator + * @param DateTime $dateTime + * @param Product $productResource + * @param Helper $salesResourceHelper + * @param string|null $connectionName * @param array $ignoredProductTypes - * @param string $connectionName + * @param StoreManagerInterface|null $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Reports\Model\FlagFactory $reportsFlagFactory, - \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\Catalog\Model\ResourceModel\Product $productResource, - \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper, - $connectionName = null, - array $ignoredProductTypes = [] + Context $context, + LoggerInterface $logger, + TimezoneInterface $localeDate, + FlagFactory $reportsFlagFactory, + Validator $timezoneValidator, + DateTime $dateTime, + Product $productResource, + Helper $salesResourceHelper, + ?string $connectionName = null, + array $ignoredProductTypes = [], + ?StoreManagerInterface $storeManager = null ) { parent::__construct( $context, @@ -73,6 +94,7 @@ public function __construct( $this->_productResource = $productResource; $this->_salesResourceHelper = $salesResourceHelper; $this->ignoredProductTypes = array_merge($this->ignoredProductTypes, $ignoredProductTypes); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -92,123 +114,161 @@ protected function _construct() * @param string|int|\DateTime|array|null $to * @return $this * @throws \Exception - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function aggregate($from = null, $to = null) { $connection = $this->getConnection(); - //$this->getConnection()->beginTransaction(); - - try { - if ($from !== null || $to !== null) { - $subSelect = $this->_getTableDateRangeSelect( - $this->getTable('sales_order'), - 'created_at', - 'updated_at', - $from, - $to - ); - } else { - $subSelect = null; - } - - $this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect); - // convert dates to current admin timezone - $periodExpr = $connection->getDatePartSql( - $this->getStoreTZOffsetQuery( - ['source_table' => $this->getTable('sales_order')], - 'source_table.created_at', - $from, - $to - ) - ); - $select = $connection->select(); - - $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']); - - $columns = [ - 'period' => $periodExpr, - 'store_id' => 'source_table.store_id', - 'product_id' => 'order_item.product_id', - 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'), - 'product_price' => new \Zend_Db_Expr( - 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' . - '* MIN(source_table.base_to_global_rate)' - ), - 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'), - ]; - - $select->from( - ['source_table' => $this->getTable('sales_order')], - $columns - )->joinInner( - ['order_item' => $this->getTable('sales_order_item')], - 'order_item.order_id = source_table.entity_id', - [] - )->joinLeft( - ['order_item_parent' => $this->getTable('sales_order_item')], - 'order_item.parent_item_id = order_item_parent.item_id', - [] - )->where( - 'source_table.state != ?', - \Magento\Sales\Model\Order::STATE_CANCELED - )->where( - 'order_item.product_type NOT IN(?)', - $this->ignoredProductTypes - ); + $this->clearByDateRange($from, $to); + foreach ($this->storeManager->getStores(true) as $store) { + $this->processStoreAggregate($store->getId(), $from, $to); + } - if ($subSelect !== null) { - $select->having($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); - } - - $select->useStraightJoin(); - // important! - $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); - $connection->query($insertQuery); - - $columns = [ - 'period' => 'period', - 'store_id' => new \Zend_Db_Expr(\Magento\Store\Model\Store::DEFAULT_STORE_ID), - 'product_id' => 'product_id', - 'product_name' => new \Zend_Db_Expr('MIN(product_name)'), - 'product_price' => new \Zend_Db_Expr('MIN(product_price)'), - 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'), - ]; - - $select->reset(); - $select->from( - $this->getMainTable(), - $columns - )->where( - 'store_id <> ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); + $columns = [ + 'period' => 'period', + 'store_id' => new \Zend_Db_Expr(Store::DEFAULT_STORE_ID), + 'product_id' => 'product_id', + 'product_name' => new \Zend_Db_Expr('MIN(product_name)'), + 'product_price' => new \Zend_Db_Expr('MIN(product_price)'), + 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'), + ]; - if ($subSelect !== null) { - $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); - } - - $select->group(['period', 'product_id']); - $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); - $connection->query($insertQuery); - - // update rating - $this->_updateRatingPos(self::AGGREGATION_DAILY); - $this->_updateRatingPos(self::AGGREGATION_MONTHLY); - $this->_updateRatingPos(self::AGGREGATION_YEARLY); - $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE); - } catch (\Exception $e) { - throw $e; + $select = $connection->select(); + $select->reset(); + $select->from( + $this->getMainTable(), + $columns + )->where( + 'store_id <> ?', + Store::DEFAULT_STORE_ID + ); + $subSelect = $this->getRangeSubSelect($from, $to); + if ($subSelect !== null) { + $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); } + $select->group(['period', 'product_id']); + $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); + $connection->query($insertQuery); + + $this->_updateRatingPos(self::AGGREGATION_DAILY); + $this->_updateRatingPos(self::AGGREGATION_MONTHLY); + $this->_updateRatingPos(self::AGGREGATION_YEARLY); + $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE); + return $this; } + /** + * Clear aggregate existing data by range + * + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return void + * @throws LocalizedException + */ + private function clearByDateRange($from = null, $to = null): void + { + $subSelect = $this->getRangeSubSelect($from, $to); + $this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect); + } + + /** + * Get report range sub-select + * + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return Select|null + */ + private function getRangeSubSelect($from = null, $to = null): ?Select + { + $subSelect = null; + if ($from !== null || $to !== null) { + $subSelect = $this->_getTableDateRangeSelect( + $this->getTable('sales_order'), + 'created_at', + 'updated_at', + $from, + $to + ); + } + + return $subSelect; + } + + /** + * Calculate report aggregate per store + * + * @param int|null $storeId + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return void + * @throws LocalizedException + */ + private function processStoreAggregate(?int $storeId, $from = null, $to = null): void + { + $connection = $this->getConnection(); + + // convert dates to current admin timezone + $periodExpr = $connection->getDatePartSql( + $this->getStoreTZOffsetQuery( + ['source_table' => $this->getTable('sales_order')], + 'source_table.created_at', + $from, + $to + ) + ); + $select = $connection->select(); + $subSelect = $this->getRangeSubSelect($from, $to); + + $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']); + + $columns = [ + 'period' => $periodExpr, + 'store_id' => 'source_table.store_id', + 'product_id' => 'order_item.product_id', + 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'), + 'product_price' => new \Zend_Db_Expr( + 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' . + '* MIN(source_table.base_to_global_rate)' + ), + 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'), + ]; + + $select->from( + ['source_table' => $this->getTable('sales_order')], + $columns + )->joinInner( + ['order_item' => $this->getTable('sales_order_item')], + 'order_item.order_id = source_table.entity_id', + [] + )->joinLeft( + ['order_item_parent' => $this->getTable('sales_order_item')], + 'order_item.parent_item_id = order_item_parent.item_id', + [] + )->where( + "source_table.entity_id IN (SELECT entity_id FROM " . $this->getTable('sales_order') . + " WHERE store_id = " . $storeId . + " AND state != '" . \Magento\Sales\Model\Order::STATE_CANCELED . "'" . + ($subSelect !== null ? + " AND " . $this->_makeConditionFromDateRangeSelect($subSelect, $periodExpr) : + '') . ")" + )->where( + 'order_item.product_type NOT IN(?)', + $this->ignoredProductTypes + ); + + $select->useStraightJoin(); + // important! + $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); + $connection->query($insertQuery); + } + /** * Update rating position * * @param string $aggregation * @return $this + * @throws LocalizedException */ protected function _updateRatingPos($aggregation) { diff --git a/app/code/Magento/Sales/README.md b/app/code/Magento/Sales/README.md index 69068f5bfc5dc..9d4f6c369587a 100644 --- a/app/code/Magento/Sales/README.md +++ b/app/code/Magento/Sales/README.md @@ -1,8 +1,10 @@ # Overview + ## Purpose of module Magento\Sales module is responsible for order processing and appearance in system, Magento\Sales module manages next system entities and flows: + * order management; * invoice management; * shipment management (including tracks management); @@ -10,10 +12,12 @@ Magento\Sales module manages next system entities and flows: Magento\Sales module is required for Magento\Checkout module to perform checkout operations. # Deployment + ## System requirements The Magento_Sales module does not have any specific system requirements. Depending on how many orders are being placed, there might be consideration for the database size ## Install + The Magento_Sales module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml new file mode 100644 index 0000000000000..63082bc505ecb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateToNewOrderPageExistingCustomerActionGroup"> + <annotations> + <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Filters the grid for the provided Customer. Clicks on the Customer. Validates that the Page Title is present and correct.</description> + </annotations> + <arguments> + <argument name="customer"/> + </arguments> + + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <waitForPageLoad stepKey="waitForCustomerGridLoad"/> + + <!--Clear grid filters--> + <conditionalClick selector="{{AdminOrderCustomersGridSection.resetButton}}" dependentSelector="{{AdminOrderCustomersGridSection.resetButton}}" visible="true" stepKey="clearExistingCustomerFilters"/> + <fillField userInput="{{customer.email}}" selector="{{AdminOrderCustomersGridSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminOrderCustomersGridSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForFilteredCustomerGridLoad"/> + <click selector="{{AdminOrderCustomersGridSection.firstRow}}" stepKey="clickOnCustomer"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.xml new file mode 100644 index 0000000000000..438407e2e3468 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.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"> + <!--Navigate to create order page (New Order -> Create New Customer)--> + <actionGroup name="AdminNavigateToNewOrderPageNewCustomerActionGroup"> + <annotations> + <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Clicks on 'Create New Customer'.</description> + </annotations> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml new file mode 100644 index 0000000000000..90e52f56059c6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectStoreDuringOrderCreationActionGroup"> + <annotations> + <description>Selects provided Store View.</description> + </annotations> + <arguments> + <argument name="storeView" defaultValue="_defaultStore"/> + </arguments> + + <waitForElementClickable selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="waitForStoreOption" /> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="selectStoreView"/> + <waitForPageLoad stepKey="waitForLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml index 2349be636cfa7..2afa13f86000e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml @@ -21,9 +21,11 @@ <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> <waitForPageLoad stepKey="waitForStoresPageOpened"/> - <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> - <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable stepKey="waitForStoreClickable" selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}"/> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="chooseStore"/> <waitForPageLoad stepKey="waitForStoreToAppear"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> <waitForPageLoad stepKey="waitForProductsListForOrder"/> <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml index 7277a75c9bc73..71379830e7a42 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml @@ -21,8 +21,10 @@ <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> <waitForPageLoad stepKey="waitForStoresPageOpened"/> - <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> + <waitForElementClickable selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}" stepKey="waitForStoreClickable" /> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="chooseStore"/> <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> <waitForPageLoad stepKey="waitForProductsListForOrder"/> <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml index 220d47e0495f9..4ce642096cff3 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="NavigateToNewOrderPageExistingCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageExistingCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageExistingCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Filters the grid for the provided Customer. Clicks on the Customer. Selects the provided Store View, if present. Validates that the Page Title is present and correct.</description> </annotations> @@ -32,6 +32,12 @@ <waitForPageLoad stepKey="waitForCreateOrderPageLoad"/> <!-- Select store view if appears --> + <!-- + Adding wait for 5 seconds to make sure AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name) + renders properly. Unfortunately can not add waitForElement because in some scenarios where this action group + is used the step with Store selection is absent. That is why click is conditional. + --> + <wait time="5" stepKey="wait" /> <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml index 883f1047feb79..a998f7e0b8791 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="NavigateToNewOrderPageExistingCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="NavigateToNewOrderPageExistingCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageExistingCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>EXTENDS: NavigateToNewOrderPageExistingCustomerActionGroup. Clicks on the provided Store View.</description> </annotations> @@ -16,7 +16,7 @@ <argument name="storeView" defaultValue="_defaultStore"/> </arguments> - <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> <waitForPageLoad stepKey="waitForLoad" after="selectStoreView"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml index 73a4da42eb093..9e396e98d4dce 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml @@ -9,7 +9,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Navigate to create order page (New Order -> Create New Customer)--> - <actionGroup name="NavigateToNewOrderPageNewCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageNewCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageNewCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Clicks on 'Create New Customer'. Select the provided Store View, if present. Validates that Page Title is present and correct.</description> </annotations> @@ -22,6 +22,13 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + <!-- + Adding wait for 5 seconds to make sure AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name) + renders properly. Unfortunately can not add waitForElement because in some scenarios where this action group + is used the step with Store selection is absent. That is why click is conditional. + --> + <wait time="5" stepKey="wait" /> <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml index 8141d7fb534c5..fb1da85bfb9e7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml @@ -35,4 +35,16 @@ <data key="scope_id">1</data> <data key="value">0</data> </entity> + <entity name="DisableFreeOrderPaymentAutomaticInvoiceAction"> + <data key="path">payment/free/payment_action</data> + <data key="scope">default</data> + <data key="scope_id">1</data> + <data key="value">0</data> + </entity> + <entity name="EnableFreeOrderPaymentAutomaticInvoiceAction"> + <data key="path">payment/free/payment_action</data> + <data key="scope">default</data> + <data key="scope_id">1</data> + <data key="value">1</data> + </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index f17172a1f75c8..499f067a3e1c8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -30,5 +30,7 @@ <element name="purchaseOrderNumber" type="input" selector="#po_number"/> <element name="freePaymentLabel" type="text" selector="#order-billing_method_form label[for='p_method_free']"/> <element name="paymentLabelWithRadioButton" type="text" selector="#order-billing_method_form .admin__field-option input[title='{{paymentMethodName}}'] + label" parameterized="true"/> + <element name="checkoutPaymentMethod" type="radio" selector="//div[@class='payment-method _active']/div/input[@id= '{{methodName}}']" parameterized="true"/> + <element name="storedCard" type="radio" selector="#p_method_payflowpro_cc_vault" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml index 6af3c5bf4f585..2a12cb7593c4b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml @@ -13,5 +13,6 @@ <element name="total" type="text" selector="//tr[contains(@class,'row-totals')]/td[contains(text(), '{{total}}')]/following-sibling::td/span[contains(@class, 'price')]" parameterized="true"/> <element name="grandTotal" type="text" selector="//tr[contains(@class,'row-totals')]/td/strong[contains(text(), 'Grand Total')]/parent::td/following-sibling::td//span[contains(@class, 'price')]"/> <element name="appendComments" type="checkbox" selector="input#notify_customer"/> + <element name="emailOrderConfirmation" type="checkbox" selector="input#send_confirmation"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 4ac48485127b1..efa2cec38a961 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -46,5 +46,12 @@ <element name="orderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[contains(text(), '{{orderId}}')]" parameterized="true"/> <element name="exactOrderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[text()='{{orderId}}']" parameterized="true"/> <element name="orderIdByIncrementId" type="text" selector="//input[@class='admin__control-checkbox' and @value={{incrId}}]/parent::label/parent::td/following-sibling::td" parameterized="true"/> + <element name="orderSubtotal" type="input" selector="//tbody//tr[@class='col-0']//td[@class='label' and contains(text(),'Subtotal')]/..//td//span[@class='price']"/> + <element name="orderPageSearchProductBySKU" type="input" selector="#sales_order_create_search_grid_filter_sku"/> + <element name="searchProductButtonOrderPage" type="button" selector="//div[@class='order-details order-details-existing-customer']//button[@title='Search']" timeout="60"/> + <element name="selectGiftsWrappingDesign" type="select" selector="//label[@class='admin__field-label' and text()='Gift Wrapping Design']/..//select"/> + <element name="giftsWrappingForOrderExclTaxPrice" type="text" selector="//td[contains(text(),'Gift Wrapping for Order (Excl. Tax)')]/..//span[@class='price' and text()='${{price}}']" parameterized="true"/> + <element name="giftsWrappingForOrderInclTaxPrice" type="text" selector="//td[contains(text(),'Gift Wrapping for Order (Incl. Tax)')]/..//span[@class='price' and text()='${{price}}']" parameterized="true"/> + <element name="secondRow" type="button" selector="tr.data-row:nth-of-type(2)"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml index 0bc18af8c84af..b5cd45010c9a0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml @@ -32,5 +32,9 @@ <element name="applyCoupon" type="input" selector="#coupons:code"/> <element name="submitOrder" type="button" selector="#submit_order_top_button" timeout="60"/> <element name="orderID" type="text" selector="|Order # (\d+)|"/> + <element name="selectProductNextPage" type="button" selector="//button[@title='Next page']"/> + <element name="selectProductPreviousPage" type="button" selector="//button[@class='action-previous']"/> + <element name="displayedProducts" type="text" selector="//input[@class='checkbox admin__control-checkbox']/../../..//td[contains(@class,'col-sku') and contains(text(),'test')]"/> + <element name="pageNumber" type="input" selector="//input[@id='sales_order_create_search_grid_page-current' and @value='{{page_index}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml index 76b5e2ad81bd1..1ecacb3e11de8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16008"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml index 1fef956505771..b8f2d64c3dc98 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16007"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml index d569cb96707d8..9320e62c8ce27 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml @@ -19,6 +19,7 @@ <severity value="MAJOR"/> <group value="sales"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="simpleCustomer"/> @@ -32,7 +33,7 @@ </after> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPageWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPageWithExistingCustomer"> <argument name="customer" value="$simpleCustomer$"/> </actionGroup> <!-- Add to order maximum available quantity - 1 --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 5964c656aa99d..83253025e90c5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94470"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -42,7 +43,7 @@ </after> <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/> <!--Check if order can be submitted without the required fields including email address--> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml index af98ac1a04d2d..af97062d565b9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml @@ -44,7 +44,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml index a3c3ff90f39a9..5e6d7fd63e572 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml @@ -114,7 +114,7 @@ </after> <!-- Create new customer order --> <comment userInput="Create new customer order" stepKey="createNewCustomerOrderComment"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <!-- Add bundle product to order and check product price in grid --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml index 35b19b7b69228..6bbb4369b8d12 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16071"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> @@ -48,7 +49,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml index 20ea41ef68fd9..238e3416e32b8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml @@ -46,7 +46,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml index a89d29a54e312..ec4f61549de97 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-16067"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -42,7 +43,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!-- Disable Free Shipping --> <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> @@ -52,7 +53,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml index b2bfa93678dfd..0d700204658a0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-15290"/> <useCaseId value="MC-15289"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> @@ -26,7 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="openNewOrder"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="openNewOrder"/> <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="Retailer" stepKey="selectCustomerGroup"/> <waitForPageLoad stepKey="waitForPageLoad"/> <grabValueFrom selector="{{AdminOrderFormAccountSection.group}}" stepKey="grabGroupValue"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml index 4d50217d3615e..dc9bb77e9d87d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml @@ -28,7 +28,7 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AssertAdminBillingAddressFieldsOnOrderCreateFormActionGroup" stepKey="assertFieldsFilled"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml index c29b2aa167ed7..55aa0ab6cc732 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml @@ -41,7 +41,7 @@ </after> <!-- Create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml index 141bf27c8f184..97ef82e2d64f0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml @@ -42,7 +42,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml index ff5dc0e36fdbd..db48fda29492b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-28444"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> @@ -100,7 +101,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml index 2a30c814f6a13..4aa1e8eafcdeb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml @@ -30,7 +30,7 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Flat Rate" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> - <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> </before> <after> <!-- Delete data --> @@ -40,7 +40,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> @@ -90,6 +90,10 @@ <!-- Assert Credit Memo button --> <seeElement selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="assertCreditMemoButton"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeAdminOrderStatus"> + <argument name="status" value="{{OrderStatus.processing}}"/> + </actionGroup> + <!--Assert refund in Credit Memo Tab--> <click selector="{{AdminOrderDetailsOrderViewSection.creditMemos}}" stepKey="clickCreditMemoTab"/> <waitForPageLoad stepKey="waitForTabLoad"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml index 36d319bf71125..06a0376c8f538 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml @@ -47,7 +47,7 @@ </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml index f122665c6fca7..90d2db36075a4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml @@ -43,7 +43,7 @@ </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml index 8b8789d488b9c..c535c458e14e2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <group value="sales"/> <testCaseId value="MC-35848"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -34,7 +35,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml index 219a3fcb4a04e..f44e7bd3b06e1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml @@ -67,7 +67,7 @@ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddBundleProductToOrderAndCheckPriceInGridActionGroup" stepKey="addBundleProductToOrder"> @@ -94,11 +94,16 @@ <actionGroup ref="AdminOpenCreditMemoFromOrderPageActionGroup" stepKey="openCreditMemo" /> <scrollTo selector="{{AdminCreditMemoViewTotalSection.subtotal}}" stepKey="scrollToTotal"/> + + <!-- Perform asserts --> <actionGroup ref="AssertAdminCreditMemoViewPageTotalsActionGroup" stepKey="assertCreditMemoViewPageTotals"> <argument name="subtotal" value="$0.00"/> <argument name="adjustmentRefund" value="$10.00"/> <argument name="adjustmentFee" value="$0.00"/> <argument name="grandTotal" value="$10.00"/> </actionGroup> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="AssertAdminOrderStatus"> + <argument name="status" value="{{OrderStatus.processing}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml index 13b91fa605bca..cf17c1f549959 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml @@ -30,24 +30,24 @@ </before> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createSimpleCustomer$$"/> </actionGroup> <actionGroup ref="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" stepKey="clickAddProducts"> <argument name="product" value="$createSimpleProduct$"/> </actionGroup> - - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToCheckColumn"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectProduct"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyProductChecked"/> - + <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> </after> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml index 6570280b1118b..51ead26cd6cf6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml @@ -28,7 +28,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml index 6475688daa46c..05121b1f8d3ee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml @@ -36,7 +36,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <!--Step 1: Create new order for customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <!--Step 2: Add product1 to the order--> @@ -75,7 +75,7 @@ <!--Disable free shipping method --> <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml index 476eb161936fd..a5bd0d98572f9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml @@ -31,7 +31,7 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml index 903429e6a0b8f..cb0a6835f9231 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml @@ -42,7 +42,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml index 5c49d29ddf22e..748fb65680a8c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -80,7 +80,7 @@ </before> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml index 5afe576145873..f0ea70bb1b48a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml @@ -15,6 +15,7 @@ <description value="Check if checked Append Comment checkbox isn't reset after shippinhg method selectiong"/> <severity value="MAJOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -33,7 +34,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml index 1d53a7d2d5e00..2df8c35c8804d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml @@ -16,6 +16,7 @@ <features value="Sales"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -26,7 +27,7 @@ </actionGroup> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderPressKeyEnterActionGroup" stepKey="addFirstConfigurableProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml index 4096a2473e979..58b3c94124c49 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="AC-2040"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -27,7 +28,7 @@ </actionGroup> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addFirstConfigurableProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml index 649956ef8e1a2..9aa1eb536aa3b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml @@ -37,7 +37,7 @@ </after> <!--Create order.--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup" /> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$simpleProduct$$"/> <argument name="productQty" value="{{SimpleProduct.quantity}}"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml index 199fa01b25376..e5678f049c40b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml @@ -17,6 +17,7 @@ <stories value="Create order in Admin"/> <severity value="MINOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> @@ -34,7 +35,7 @@ <deleteData createDataKey="createCustomer" stepKey="deleteSimpleCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml index ac1d1dd841ef7..97858b530c754 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml @@ -42,7 +42,7 @@ </after> <!-- Start Order Creation --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> @@ -96,7 +96,7 @@ <magentoCLI stepKey="setCustomRecordsLimit" command="config:set admin/grid/records_limit 3"/> <!-- Start order creation again --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index cb2d33420fb0c..bf6933458d1cd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92925"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -31,7 +32,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <createData entity="DisabledMinimumOrderAmount" stepKey="disableMinimumOrderAmount"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml index 6e8ec14fc67cb..196246cdfb800 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml @@ -15,6 +15,7 @@ <features value="Sales"/> <severity value="BLOCKER"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <!--Set default flat rate shipping method settings--> @@ -53,7 +54,7 @@ </actionGroup> <!--Step 3: Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml index b19e1fc8eff9d..f39ee0f0991e1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml @@ -15,6 +15,7 @@ <features value="Sales"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <!--Create test data.--> @@ -42,7 +43,7 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Create order.--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$customer$"/> </actionGroup> <actionGroup ref="AdminAddSimpleProductWithCustomOptionFileToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml index a26b2dcd06ae5..4097972cc9e92 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="Sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> @@ -28,7 +29,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml index d7049ba6b8a69..a0f777e7ccc36 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml @@ -6,7 +6,7 @@ */ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCreateOrdersAndCheckGridsTest"> <annotations> <stories value="Create orders and check grids"/> @@ -16,6 +16,7 @@ <useCaseId value="ACP2E-1367" /> <testCaseId value="AC-7106" /> <group value="sales"/> + <group value="async_operations" /> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -47,12 +48,14 @@ <requiredEntity createDataKey="createGuestCartOne"/> </updateData> - <magentoCLI command="cron:run --group=default" stepKey="runCronOne"/> + <magentoCron groups="default" stepKey="runCronOne"/> <createData entity="Invoice" stepKey="invoiceOrderOne"> <requiredEntity createDataKey="createGuestCartOne"/> </createData> + <magentoCron groups="default" stepKey="runCronTwo"/> + <createData entity="GuestCart" stepKey="createGuestCartTwo"/> <createData entity="SimpleCartItem" stepKey="addCartItemTwo"> <requiredEntity createDataKey="createGuestCartTwo"/> @@ -65,11 +68,13 @@ <requiredEntity createDataKey="createGuestCartTwo"/> </updateData> + <magentoCron groups="default" stepKey="runCronThree"/> + <createData entity="Shipment" stepKey="shipOrderOne"> <requiredEntity createDataKey="createGuestCartOne"/> </createData> - <magentoCLI command="cron:run --group=default" stepKey="runCronTwo"/> + <magentoCron groups="default" stepKey="runCronFour"/> <createData entity="GuestCart" stepKey="createGuestCartThree"/> <createData entity="SimpleCartItem" stepKey="addCartItemThree"> @@ -83,19 +88,25 @@ <requiredEntity createDataKey="createGuestCartThree"/> </updateData> + <magentoCron groups="default" stepKey="runCronFive"/> + <createData entity="CreditMemo" stepKey="refundOrderOne"> <requiredEntity createDataKey="createGuestCartOne"/> </createData> + <magentoCron groups="default" stepKey="runCronSix"/> + <createData entity="Invoice" stepKey="invoiceOrderThree"> <requiredEntity createDataKey="createGuestCartThree"/> </createData> + <magentoCron groups="default" stepKey="runCronSeven"/> + <createData entity="Shipment" stepKey="shipOrderTwo"> <requiredEntity createDataKey="createGuestCartTwo"/> </createData> - <magentoCLI command="cron:run --group=default" stepKey="runCronThree"/> + <magentoCron groups="default" stepKey="runCronEight"/> <createData entity="Invoice" stepKey="invoiceOrderTwo"> <requiredEntity createDataKey="createGuestCartTwo"/> @@ -109,34 +120,27 @@ <requiredEntity createDataKey="createGuestCartTwo"/> </createData> - <magentoCLI command="cron:run --group=default" stepKey="runCronFour"/> - <createData entity="CreditMemo" stepKey="refundOrderThree"> <requiredEntity createDataKey="createGuestCartThree"/> </createData> - <magentoCLI command="cron:run --group=default" stepKey="runCronFive"/> - <magentoCLI command="cron:run --group=default" stepKey="runCronSix"/> - <magentoCLI command="cron:run --group=default" stepKey="runCronSeven"/> - <magentoCLI command="cron:run --group=default" stepKey="runCronEight"/> - <magentoCLI command="cron:run --group=default" stepKey="runCronNine"/> + <magentoCron groups="default" stepKey="runCronNine"/> + + <magentoCron groups="default" stepKey="runCronTen"/> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> - <waitForPageLoad time="60" stepKey="waitForGrid"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> - <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartOne.return$)}}" stepKey="getOrderOneId"/> - <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartTwo.return$)}}" stepKey="getOrderTwoId"/> - <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartThree.return$)}}" stepKey="getOrderThreeId"/> - <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderOne"> <argument name="entityId" value="$createGuestCartOne.return$"/> </actionGroup> <waitForPageLoad time="30" stepKey="waitForPageLoadOne"/> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdOne"/> + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageOne"/> <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask1" after="openInvoicesTabOrdersPageOne"/> <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedOne"/> @@ -152,6 +156,8 @@ <waitForPageLoad time="30" stepKey="waitForPageLoadTwo"/> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdTwo"/> + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageTwo"/> <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask2" after="openInvoicesTabOrdersPageTwo"/> <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedTwo"/> @@ -167,6 +173,8 @@ <waitForPageLoad time="30" stepKey="waitForPageLoadThree"/> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdThree"/> + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageThree"/> <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask3" after="openInvoicesTabOrdersPageThree"/> <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedThree"/> @@ -175,5 +183,24 @@ <seeElement selector="{{AdminOrderShipmentsTabSection.viewShipment}}" stepKey="seeForShipmentTabOpenedThree"/> <actionGroup ref="AdminGoToCreditMemoTabActionGroup" stepKey="goToCreditMemoTabThree"/> <see selector="{{AdminOrderCreditMemosTabSection.gridRowCell('1', 'Status')}}" userInput="Refunded" stepKey="seeCreditMemoStatusInGridThree"/> + + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdOne"> + <argument name="orderId" value="{$grabOrderIdOne}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridOne"/> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdTwo"> + <argument name="orderId" value="{$grabOrderIdTwo}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridTwo"/> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdThree"> + <argument name="orderId" value="{$grabOrderIdThree}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridThree"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml index 70a292f4ee269..3e6bf9ac36d39 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml @@ -17,6 +17,7 @@ <testCaseId value="ACP2E-188"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml index 3480fdc4dc9e6..b6dbd092d411f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml @@ -48,7 +48,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!--Create new order with existing customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml index 741928d3baa85..8af5f7a9dc38e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -24,7 +24,7 @@ <field key="price">100</field> </createData> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <createData entity="DisableFlatRateShippingMethodConfig" stepKey="disableFlatRate"/> + <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -34,14 +34,14 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!--Create new order with existing customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml index 10b911e2d8f2a..29fb7e9a8836c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml @@ -44,7 +44,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml index c9af87cdb1150..36b2a51e9ec6c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-72096"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml index 794d09226d87f..1bd342fb973c7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-39905"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml index 853fa5822f799..38e79e5deb4c1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16184"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml index 77a7aedbc6c7b..a01451052dc33 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16186"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml index 23e71dcb03a0e..f10771c690b29 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16185"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml index e8b842a48890e..20211900cbbee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16182"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml index 38b85828c3421..65812e8dbeef6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-39500"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml new file mode 100644 index 0000000000000..208e542f66352 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminOrderInformationCommentHintTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin order information page"/> + <title value="Dialog box error message when submitting comment in Order Details page"/> + <description value="Check comment hint and dialog box error message visible"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8374"/> + <useCaseId value="ACP2E-1775"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create order --> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="createFirstOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Open Admin Order page --> + <actionGroup ref="AdminOpenOrderViewPageByOrderIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="$createCustomerCart.return$"/> + </actionGroup> + + <!--Go to submit comment section and verify the comment hint text--> + <scrollTo selector="#order_history_block" stepKey="scrollToSection"/> + <see userInput="A status change or comment text is required to submit a comment." stepKey="seeMessageNotesForThisOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml index fea3fe68fd522..70f6ed7e8a5cd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml @@ -124,7 +124,7 @@ </after> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml index 2299a12546849..985936840dba2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml @@ -32,9 +32,7 @@ </before> <after> - - <!--Remove default flat rate shipping method settings--> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Delete product--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <!--Delete customer--> @@ -61,7 +59,10 @@ <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> <grabTextFrom selector="{{OrdersGridSection.orderID}}" stepKey="orderNumber"/> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToSalesOrderPage1"/> - <click selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="clickSelectAll"/> + <scrollToTopOfPage stepKey="scrollToTop" /> + <actionGroup ref="AdminClearGridFiltersActionGroup" stepKey="clearFilters" /> + <waitForElementClickable selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="waitForSelectAllClickable" /> + <checkOption selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="clickSelectAll"/> <openNewTab stepKey="openNewTab"/> <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml index e0576f94347cf..0332a59af87aa 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16187"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml index 9c3356760341f..a969f6aee867b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml @@ -74,8 +74,10 @@ <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="addProductToCart"/> <!--Create new order for existing Customer And Store--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml index dc96da653d6d6..0e4cd1626b4fc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MC-38113"/> <severity value="MAJOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="category"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml index 121b1a13333af..5a663edebc4d0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-99691"/> <group value="sales"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml index 772539dc63baf..6ade37148020c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml index 8e3a87aa8f67f..6214078e2ee5c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml index dce66e929b2dc..ed5e2ae63c664 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml index d5805176a0649..6808f249480e2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml @@ -16,6 +16,7 @@ <description value="Admin should not be able print packing slips until shipment was not created"/> <severity value="MINOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -44,7 +45,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml index 387e2b840384f..a9d1e8716d600 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml index deffdb63c4634..a5bc987f03d10 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml index f8136a9071a1a..7cbf97cd04fee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-36337"/> <useCaseId value="MAGETWO-99320"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer, category, product and log in --> @@ -39,7 +40,7 @@ </after> <!-- Create new order and choose an existing customer --> <comment userInput="Create new order and choose an existing customer" stepKey="createOrderAndAddCustomer"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!-- Add simple product to order --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml index 9ac7da076b10b..81209408fada7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml index 1ad7b3db21270..1f4ea374605f4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26545"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> @@ -104,7 +105,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$simpleCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index a517d310f6a5b..686ec150281e0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92980"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -27,7 +28,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -35,7 +36,6 @@ <!--Create order via Admin--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <actionGroup ref="AssertAdminPageTitleActionGroup" stepKey="seeIndexPageTitle"> <argument name="value" value="Orders"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 7e18c35820117..7963e28e8b5ab 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -16,6 +16,7 @@ <severity value="AVERAGE"/> <group value="sales"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -31,7 +32,6 @@ <!--Create order via Admin--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <actionGroup ref="AssertAdminPageTitleActionGroup" stepKey="seeIndexPageTitle"> <argument name="value" value="Orders"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml new file mode 100644 index 0000000000000..6ead066235caf --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest"> + <annotations> + <stories value="Verify Append Comments check-box checked"/> + <title value="Verify Append Comments check-box checked when shipping method is selected"/> + <description value="Verify Append Comments check-box checked when shipping method is selected"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5606"/> + </annotations> + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Create order --> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <!-- Add product to order --> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AdminAddCommentOnCreateOrderPageActionGroup" stepKey="provideComment"> + <argument name="comment" value="Test Order Comment"/> + </actionGroup> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.appendComments}}" stepKey="checkAppendCommentsCheckboxIsCheckedAfterCommentProvided"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.emailOrderConfirmation}}" stepKey="checkEmailOrderConfirmationCheckboxIsCheckedAfterCommentProvided"/> + <scrollTo selector="{{AdminOrderFormPaymentSection.header}}" stepKey="scrollUp"/> + <actionGroup ref="AdminSelectFlatRateShippingMethodOnCreateOrderPageActionGroup" stepKey="selectFlatRate"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.appendComments}}" stepKey="againCheckAppendCommentsCheckboxIsCheckedAfterCommentProvided"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml index b0c6b3a2fc6ca..62d1311647ec3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml @@ -15,6 +15,7 @@ <title value="Verify field to filter"/> <description value="Verify not appear fields to filter on Orders grid if it disables in columns dropdown."/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml index c6aa91facb383..e504776754621 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml @@ -14,6 +14,7 @@ <description value="Admin opens order with restricted access"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="Product"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml index 88e3ada61068a..21baa73b03698 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml @@ -111,7 +111,7 @@ <see selector="{{AdminOrderStatusGridSection.gridCell('1', 'State Code and Title')}}" userInput="new[{{defaultOrderStatus.label}}]" stepKey="seeOrderStatusInOrderGrid"/> <!-- Create order and grab order id --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml new file mode 100644 index 0000000000000..228512fb03a50 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckPagerInOrderAddProductsGridTest"> + <annotations> + <stories value="Check Pager in order add products grid"/> + <title value="Check Pager in order add products grid"/> + <description value="Check Pager in order add products grid"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7315"/> + </annotations> + <before> + <!-- Step1: Create new category and 21 products --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct4"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct5"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct6"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct7"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct8"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct9"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct10"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct11"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct13"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct14"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct15"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct16"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct17"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct18"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct19"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct20"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct21"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete created category and products --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="createSimpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="createSimpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="createSimpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="createSimpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="createSimpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="createSimpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="createSimpleProduct10" stepKey="deleteSimpleProduct10"/> + <deleteData createDataKey="createSimpleProduct11" stepKey="deleteSimpleProduct11"/> + <deleteData createDataKey="createSimpleProduct12" stepKey="deleteSimpleProduct12"/> + <deleteData createDataKey="createSimpleProduct13" stepKey="deleteSimpleProduct13"/> + <deleteData createDataKey="createSimpleProduct14" stepKey="deleteSimpleProduct14"/> + <deleteData createDataKey="createSimpleProduct15" stepKey="deleteSimpleProduct15"/> + <deleteData createDataKey="createSimpleProduct16" stepKey="deleteSimpleProduct16"/> + <deleteData createDataKey="createSimpleProduct17" stepKey="deleteSimpleProduct17"/> + <deleteData createDataKey="createSimpleProduct18" stepKey="deleteSimpleProduct18"/> + <deleteData createDataKey="createSimpleProduct19" stepKey="deleteSimpleProduct19"/> + <deleteData createDataKey="createSimpleProduct20" stepKey="deleteSimpleProduct20"/> + <deleteData createDataKey="createSimpleProduct21" stepKey="deleteSimpleProduct21"/> + <!-- Delete the created customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!-- Step2: Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Step3: Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Step4: Navigate to Orders and create an order --> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <waitForPageLoad stepKey="waitForNewOrderPageOpened"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection('$$createCustomer.firstname$$')}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <!-- Step5: Click on Add Products --> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPage1"/> + <seeElement selector="{{OrdersGridSection.pageNumber('1')}}" stepKey="verifyPage1"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPage1"/> + <!-- Step6: Click on Next Page and verify products are listed on next page 2 --> + <click selector="{{OrdersGridSection.selectProductNextPage}}" stepKey="clickOnNextPageForSelectProuct"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPage2"/> + <seeElement selector="{{OrdersGridSection.pageNumber('2')}}" stepKey="verifyPage2"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPage2"/> + <!-- Step6: Click on Previous Page and verify products are listed on previous page 1 --> + <click selector="{{OrdersGridSection.selectProductPreviousPage}}" stepKey="clickOnPreviousPageForSelectProuct"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPreviousPage1"/> + <seeElement selector="{{OrdersGridSection.pageNumber('1')}}" stepKey="verifyPreviousPage1"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPreviousPage1"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml index af71fa8cf2953..26e8feb67e7b9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml @@ -18,6 +18,7 @@ <group value="sales"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -35,7 +36,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductWithQtyToOrderActionGroup" stepKey="addProductToOrder"> @@ -96,7 +97,7 @@ </actionGroup> <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="clickOrderApplyFilters"/> <dontSeeElement selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="assertThatInvoiceGridNotEmpty"/> - + <actionGroup ref="FilterInvoiceGridByOrderIdWithCleanFiltersActionGroup" stepKey="filterInvoiceByOrderId"> <argument name="orderId" value="$orderNumber"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml index 4f20df24516eb..01e6975af5e30 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml @@ -51,7 +51,7 @@ </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml index 457ee39f517ba..6962fcc9ec722 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml @@ -49,7 +49,7 @@ </after> <!-- Create order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml index fd4dc8e4aa422..76ee248b53f20 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml @@ -62,7 +62,7 @@ </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 1ae0388b206b4..c497e16e3fffd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-16161"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index f1f83dd8be3a9..a2e14c6b1c235 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92924"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 31e833f0eab7a..11675170c0be5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -31,13 +31,18 @@ <!--Prerequisites--> <!--Create store view to ensure multiple store views--> <comment userInput="Create prerequisite store view" stepKey="createStoreViewComment" before="createStoreView"/> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" before="navigateToNewOrderPage"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" before="navigateToNewOrderPage"> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Admin creates order--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment" before="navigateToNewOrderPage"/> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage" after="deleteCategory"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage" after="deleteCategory"/> - <actionGroup ref="CheckRequiredFieldsNewOrderFormActionGroup" stepKey="checkRequiredFieldsNewOrder" after="navigateToNewOrderPage"/> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore" after="navigateToNewOrderPage"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="CheckRequiredFieldsNewOrderFormActionGroup" stepKey="checkRequiredFieldsNewOrder" after="selectCustomStore"/> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="checkRequiredFieldsNewOrder"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder" after="scrollToTopOfOrderFormPage"> <argument name="product" value="SimpleProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml index b5dfa255436a7..839bef101e6e6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16104"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml index 2d4dd3220f777..e70c5cd4f350f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16155"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> @@ -72,7 +73,7 @@ </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml index 87a6dbf8fdff0..264d0b4de9315 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml @@ -19,6 +19,7 @@ <group value="sales"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml index 009037da2b50a..71724c434a883 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16103"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml new file mode 100644 index 0000000000000..1ac8b58b115b1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml @@ -0,0 +1,192 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="OrderDataGridDisplaysPurchaseDateTest"> + <annotations> + <stories value="verify purchase date format"/> + <title value="Order Data Grid displays Purchase Date in correct format"/> + <description value="Order Data Grid displays Purchase Date in correct format"/> + <testCaseId value="AC-4455"/> + <severity value="MAJOR"/> + </annotations> + <before> + <!-- Set Store Code To Urls --> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToYes"/> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <magentoCron groups="index" stepKey="reindexAllIndexes"/> + <!-- Change time zone for second website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPage"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="timeZoneName" value="Hawaii-Aleutian Standard Time (America/Adak)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <!-- Change time zone for Main website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPageSecondTime"> + <argument name="websiteName" value="Main Website"/> + <argument name="timeZoneName" value="Taipei Standard Time (Asia/Taipei)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigurationSecondTime"/> + <!-- Create category and simple product --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <!-- Open product page and assign grouped project to second website --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="openAdminProductPage"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductInWebsiteActionGroup" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + </before> + <after> + <!-- Disabled Store URLs --> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToNo"/> + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!-- Delete first customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteFirstCustomer"/> + <!-- Delete second customer --> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="gotoOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageToLoad"/> + <!-- Reset time zone for Main website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPageSecondTime"> + <argument name="websiteName" value="Main Website"/> + <argument name="timeZoneName" value="Central Standard Time (America/Chicago)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigurationSecondTime"/> + <!--set main website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setMainWebsiteAsDefault"> + <argument name="websiteName" value="Main Website"/> + </actionGroup> + <!-- Delete second website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <magentoCron groups="index" stepKey="reindex"/> + <!--reset prouct grid filter--> + <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridFilter"/> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!-- Go to product page --> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + <!-- Add Product to Shopping Cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMiniCart"/> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <comment userInput="Adding the comment to replace waitForLoadingMask2 action for preserving Backward Compatibility" stepKey="waitForLoadingMask2"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!-- Click Place Order button --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + <!-- capture date at time of Placing Order --> + <generateDate date="+2 hour" format="M j, Y" stepKey="generateDateAtFirstOrderTime"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabFirstOrderNumber"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad"/> + <!--set second website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setSecondWebsiteAsDefault"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <!-- create second Customer--> + <createData entity="Simple_US_Customer_CA" stepKey="createSecondCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="secondCustomerLogin"> + <argument name="Customer" value="$$createSecondCustomer$$" /> + </actionGroup> + <!-- Go to product page --> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPageSecondTime"/> + <waitForPageLoad stepKey="waitForCatalogPageLoadSecondTime"/> + <!-- Add Product to Shopping Cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPageSecondTime"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMiniCartSecondTime"/> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFlatRateShippingMethodSecondTime"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextSecondTime"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPaymentSecondTime"/> + <!-- Click Place Order button --> + <wait time="75" stepKey="waitBeforePlaceOrder"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrderSecondTime"/> + <!-- capture date at time of Placing Order --> + <generateDate date="+2 hour" format="M j, Y" stepKey="generateDateAtSecondOrderTime"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabSecondOrderNumber"/> + <!-- Go to admin and check order status --> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToSalesOrderPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchForFirstOrder"> + <argument name="keyword" value="{$grabFirstOrderNumber}"/> + </actionGroup> + <!--Get date from "Purchase Date" column --> + <grabTextFrom selector="{{AdminOrdersGridSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateForFirstOrderInDefaultLocale"/> + <!--Get date and time in default locale (US)--> + <executeJS function="return (new Date('{$grabPurchaseDateForFirstOrderInDefaultLocale}').toLocaleDateString('en-US',{month: 'short', day: 'numeric', year: 'numeric'} ))" stepKey="getDateMonthYearNameForFirstOrderInUS"/> + <!--Checking oder placing Date with default "Interface Locale"--> + <assertStringContainsString stepKey="checkingFirstOrderDateWithPurchaseDate"> + <expectedResult type="variable">getDateMonthYearNameForFirstOrderInUS</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <!--compare date of order with date of purchase--> + <assertStringContainsString stepKey="checkingFirstOrderDateWithDefaultInterfaceLocale1"> + <expectedResult type="variable">generateDateAtFirstOrderTime</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchForSecondOrder"> + <argument name="keyword" value="{$grabSecondOrderNumber}"/> + </actionGroup> + <!--Get date from "Purchase Date" column--> + <grabTextFrom selector="{{AdminOrdersGridSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateForSecondOrderInDefaultLocale"/> + <!--Get date and time in default locale (US)--> + <executeJS function="return (new Date('{$grabPurchaseDateForSecondOrderInDefaultLocale}').toLocaleDateString('en-US',{month: 'short', day: 'numeric', year: 'numeric'} ))" stepKey="getDateMonthYearNameForSecondOrderInUS"/> + <!--Checking Purchase Date with default "Interface Locale"--> + <assertStringContainsString stepKey="checkingSecondOrderDateWithDefaultInterfaceLocale"> + <expectedResult type="variable">getDateMonthYearNameForSecondOrderInUS</expectedResult> + <actualResult type="variable">grabPurchaseDateForSecondOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <!--compare date of order with date of purchase--> + <assertStringContainsString stepKey="checkingSecondOrderDateWithPurchaseDate"> + <expectedResult type="variable">generateDateAtSecondOrderTime</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml index 6d13738271781..9a2ca17a4162a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml @@ -15,6 +15,7 @@ <testCaseId value="AC-1577"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Add downloadable domains --> <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> @@ -82,6 +83,7 @@ <!-- Logout User and Admin --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPage"> @@ -101,7 +103,7 @@ <argument name="customer" value="$$createCustomer$$"/> </actionGroup> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStep"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml index 9ff5e61d74acc..d95d68e10fdae 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml @@ -53,7 +53,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Open new order from admin and add product--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$testProduct$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml index bf45d3305dcfd..40d96b618ac1e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml @@ -13,17 +13,19 @@ <description value="Place order on Store Front with manually filled billing address state and selected shipping address state. Check that billing address show correct state on Admin Order View page"/> <severity value="MINOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> <createData entity="Customer_UK_US" stepKey="createCustomer"/> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPage"> @@ -42,7 +44,7 @@ <argument name="customer" value="$$createCustomer$$"/> </actionGroup> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStep"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml index 8fd659a5bd6ac..3c3d25a269c9a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml @@ -14,6 +14,7 @@ <title value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <description value="Create a product and orders with a set 'Move JS code to the bottom of the page' to 'Yes' for registered customers and guests."/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableMoveJsCodeBottom.path}} {{StorefrontEnableMoveJsCodeBottom.value}}" stepKey="moveJsCodeBottomEnable"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml index 1e97703acbe00..6c1504f7453b8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-42531"/> <severity value="MINOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index 08088925d426f..d6f77a3a59202 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16167"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="LoginAsAdmin"/> @@ -93,7 +94,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index 7beebb7b5ad9a..489b68c82b138 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16166"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="LoginAsAdmin"/> @@ -88,7 +89,7 @@ </before> <after> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> @@ -114,11 +115,11 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> <argument name="Customer" value="$$createCustomer$$" /> </actionGroup> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="onCategoryPage"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToLimiter"/> @@ -209,7 +210,7 @@ <requiredEntity createDataKey="createCustomerCart"/> <requiredEntity createDataKey="createProduct20"/> </createData> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="onCheckout"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="see20Products"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickNextButton"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml index 75208d6745589..e23d3dc29299b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml @@ -174,7 +174,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Place order with options according to dataset --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml index 14de2cb556ffc..6c5e5a855dffd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-7712"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -34,15 +35,14 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" - stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="LogoutAsAdmin"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> <argument name="customer" value="Simple_US_Customer"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml index 474dc6f09ff02..f76f6cb942ea7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-34465"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -38,7 +39,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="LogoutAsAdmin"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml index 4d7c725ecab00..8ec32d963cac0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-26873"/> <severity value="MAJOR"/> <group value="Reorder_Product"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml index 218dfeab89413..cf17b6acb8d62 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml @@ -20,7 +20,7 @@ <group value="Sales"/> <skip> <issueId value="DEPRECATED">Use StorefrontOrderCommentWithHTMLTagsDisplayTest instead</issueId> - </skip> + </skip> </annotations> <before> <!-- Create customer --> @@ -50,7 +50,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml index 6f35b062aad9c..b76acf0f8ed2f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-39353"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -73,6 +74,7 @@ <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <actionGroup ref="StorefrontSelectCheckMoneyOrderActionGroup" stepKey="selectCheckmoIfNeeded" /> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> <comment userInput="BIC workaround" stepKey="grabOrderNumber"/> <actionGroup ref="StorefrontClickOrderLinkFromCheckoutSuccessPageActionGroup" stepKey="openOrderViewPage"/> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php b/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php index e0b3324449028..aa99fdcd3e485 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php @@ -83,7 +83,7 @@ class SidebarTest extends TestCase */ protected function setUp(): void { - $this->markTestIncomplete('MAGETWO-36789'); + $this->markTestSkipped('MAGETWO-36789'); $this->objectManagerHelper = new ObjectManager($this); $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); $this->httpContext = $this->createPartialMock(\Magento\Framework\App\Http\Context::class, ['getValue']); diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php index 9cb127365c055..8cdecc4e54f05 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php @@ -12,6 +12,8 @@ use Magento\Backend\Model\View\Result\RedirectFactory; use Magento\Framework\App\Request\Http; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Sales\Api\OrderRepositoryInterface; @@ -77,6 +79,12 @@ class AddCommentTest extends TestCase */ private $objectManagerMock; + /** @var JsonFactory|MockObject */ + private $jsonFactory; + + /** @var Json|MockObject */ + private $resultJson; + /** * Test setup */ @@ -94,6 +102,13 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + $this->resultJson = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $this->jsonFactory = $this->getMockBuilder(JsonFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerHelper = new ObjectManager($this); $this->addCommentController = $objectManagerHelper->getObject( AddComment::class, @@ -101,7 +116,8 @@ protected function setUp(): void 'context' => $this->contextMock, 'orderRepository' => $this->orderRepositoryMock, '_authorization' => $this->authorizationMock, - '_objectManager' => $this->objectManagerMock + '_objectManager' => $this->objectManagerMock, + 'resultJsonFactory' => $this->jsonFactory ] ); } @@ -205,4 +221,44 @@ public function executeWillNotifyCustomerDataProvider() ], ]; } + + /** + * Assert error message for empty comment value + * + * @return void + */ + public function testExecuteForEmptyCommentMessage(): void + { + $orderId = 30; + $orderStatus = 'processing'; + $historyData = [ + 'comment' => '', + 'is_customer_notified' => false, + 'status' => 'processing' + ]; + + $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderMock->expects($this->atLeastOnce())->method('getDataByKey') + ->with('status')->willReturn($orderStatus); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->requestMock->expects($this->once())->method('getPost')->with('history')->willReturn($historyData); + + $this->resultJson->expects($this->once()) + ->method('setData') + ->with( + [ + 'error' => true, + 'message' => 'Please provide a comment text or ' . + 'update the order status to be able to submit a comment for this order.' + ] + ) + ->willReturnSelf(); + $this->jsonFactory->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + + $this->assertSame($this->resultJson, $this->addCommentController->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php index c870051b93554..93bdb1f189270 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Action\Context; use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Forward; use Magento\Framework\Controller\Result\ForwardFactory; use Magento\Framework\Serialize\Serializer\Json; @@ -24,32 +25,32 @@ class DownloadCustomOptionTest extends TestCase /** * Option ID Test Value */ - const OPTION_ID = '123456'; + public const OPTION_ID = '123456'; /** * Option Code Test Value */ - const OPTION_CODE = 'option_123456'; + public const OPTION_CODE = 'option_123456'; /** * Option Product ID Value */ - const OPTION_PRODUCT_ID = 'option_test_product_id'; + public const OPTION_PRODUCT_ID = 'option_test_product_id'; /** * Option Type Value */ - const OPTION_TYPE = 'file'; + public const OPTION_TYPE = 'file'; /** * Option Value Test Value */ - const OPTION_VALUE = 'option_test_value'; + public const OPTION_VALUE = 'option_test_value'; /** * Option Value Test Value */ - const SECRET_KEY = 'secret_key'; + public const SECRET_KEY = 'secret_key'; /** * @var \Magento\Quote\Model\Quote\Item\Option|MockObject @@ -95,7 +96,7 @@ protected function setUp(): void $this->downloadMock = $this->getMockBuilder(Download::class) ->disableOriginalConstructor() - ->setMethods(['downloadFile']) + ->setMethods(['createResponse']) ->getMock(); $this->serializerMock = $this->getMockBuilder(Json::class) @@ -199,7 +200,8 @@ public function testExecute($itemOptionValues, $productOptionValues, $noRouteOcc ->willReturn($productOptionValues[self::OPTION_TYPE]); } if ($noRouteOccurs) { - $this->resultForwardMock->expects($this->once())->method('forward')->with('noroute')->willReturn(true); + $result = $this->resultForwardMock; + $this->resultForwardMock->expects($this->once())->method('forward')->with('noroute')->willReturnSelf(); } else { $unserializeResult = [self::SECRET_KEY => self::SECRET_KEY]; @@ -208,14 +210,15 @@ public function testExecute($itemOptionValues, $productOptionValues, $noRouteOcc ->with($itemOptionValues[self::OPTION_VALUE]) ->willReturn($unserializeResult); + $result = $this->getMockBuilder(ResponseInterface::class) + ->getMock(); $this->downloadMock->expects($this->once()) - ->method('downloadFile') + ->method('createResponse') ->with($unserializeResult) - ->willReturn(true); + ->willReturn($result); - $this->objectMock->expects($this->once())->method('endExecute')->willReturn(true); } - $this->objectMock->execute(); + $this->assertSame($result, $this->objectMock->execute()); } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index ac88a01ce65af..fee27fde6dc3b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -806,6 +806,67 @@ public function collectDataProvider() ], ]; + // scenario 8: 1 items, 1 invoiced, shipping covered by cart rule + // shipping amount is 0 i.e., free shipping + $result['creditmemo_with_discount_for_entire_shipping_all_prices_including_tax_free_shipping'] = [ + 'order_data' => [ + 'data_fields' => [ + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 1.36, + 'base_shipping_discount_tax_compensation_amount' => 1.36, + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + 'tax_invoiced' => 1.22, + 'base_tax_invoiced' => 1.22, + 'shipping_amount' => 0, + 'shipping_discount_amount' => 15, + 'base_shipping_amount' => 13.64, + 'discount_tax_compensation_invoiced' => 1.73, + 'base_discount_tax_compensation_invoiced' => 1.73 + ], + ], + 'creditmemo_data' => [ + 'items' => [ + 'item_1' => [ + 'order_item' => [ + 'qty_invoiced' => 1, + 'tax_invoiced' => 1.22, + 'base_tax_invoiced' => 1.22, + 'discount_tax_compensation_amount' => 1.73, + 'base_discount_tax_compensation_amount' => 1.73, + 'discount_tax_compensation_invoiced' => 1.73, + 'base_discount_tax_compensation_invoiced' => 1.73 + ], + 'is_last' => true, + 'qty' => 1, + ], + ], + 'is_last' => true, + 'data_fields' => [ + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'grand_total' => 10.45, + 'base_grand_total' => 10.45, + 'tax_amount' => 0, + 'base_tax_amount' => 0 + ], + ], + 'expected_results' => [ + 'creditmemo_items' => [ + 'item_1' => [ + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + ], + ], + 'creditmemo_data' => [ + 'grand_total' => 14.76, + 'base_grand_total' => 13.4, + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + ], + ], + ]; return $result; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php index 934bfa5f261a4..44385f8633746 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php @@ -264,6 +264,33 @@ public function testSaveWithException() $this->assertEquals($shipment, $this->subject->save($shipment)); } + public function testSaveWithValidatorException() + { + $this->expectException('Magento\Framework\Exception\CouldNotSaveException'); + $shipment = $this->createPartialMock(Shipment::class, ['getEntityId']); + $shipment->expects($this->never()) + ->method('getEntityId'); + + $mapper = $this->getMockForAbstractClass( + AbstractDb::class, + [], + '', + false, + true, + true, + ['save'] + ); + $mapper->expects($this->once()) + ->method('save') + ->willThrowException(new \Magento\Framework\Validator\Exception()); + + $this->metadata->expects($this->any()) + ->method('getMapper') + ->willReturn($mapper); + + $this->assertEquals($shipment, $this->subject->save($shipment)); + } + public function testCreate() { $shipment = $this->createMock(Shipment::class); diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php new file mode 100644 index 0000000000000..139b7c1c179bf --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php @@ -0,0 +1,245 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\ResourceModel\Report; + +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\Timezone\Validator; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Reports\Model\Flag; +use Magento\Reports\Model\FlagFactory; +use Magento\Sales\Model\ResourceModel\Helper; +use Magento\Sales\Model\ResourceModel\Report\Bestsellers; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BestsellersTest extends TestCase +{ + /** + * @var Product|MockObject + */ + protected Product $_productResource; + + /** + * @var Helper|MockObject + */ + protected Helper $_salesResourceHelper; + + /** + * @var StoreManagerInterface|MockObject + */ + protected StoreManagerInterface $storeManager; + + /** + * @var Bestsellers + */ + protected Bestsellers $report; + + /** + * @var Context + */ + protected Context $context; + + /** + * @var LoggerInterface + */ + protected LoggerInterface $logger; + + /** + * @var TimezoneInterface + */ + protected TimezoneInterface $time; + + /** + * @var FlagFactory + */ + protected FlagFactory $flagFactory; + + /** + * @var Validator + */ + protected Validator $validator; + + /** + * @var DateTime + */ + protected DateTime $date; + + /** + * @var Product + */ + protected Product $product; + + /** + * @var Helper + */ + protected Helper $helper; + + /** + * @var string + */ + protected string $connectionName = 'connection_name'; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->context = $this->createMock(Context::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->time = $this->createMock(TimezoneInterface::class); + $this->flagFactory = $this->createMock(FlagFactory::class); + $this->validator = $this->createMock(Validator::class); + $this->date = $this->createMock(DateTime::class); + $this->product = $this->createMock(Product::class); + $this->helper = $this->createMock(Helper::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + parent::setUp(); + } + + /** + * @return void + * @throws \Exception + */ + public function testAggregatePerStoreCalculationWithInterval(): void + { + $from = new \DateTime('yesterday'); + $to = new \DateTime(); + $periodExpr = 'DATE(DATE_ADD(`source_table`.`created_at`, INTERVAL -25200 SECOND))'; + $select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->exactly(2))->method('group'); + $select->expects($this->exactly(5))->method('from')->willReturn($select); + $select->expects($this->exactly(3))->method('distinct')->willReturn($select); + $select->expects($this->once())->method('joinInner')->willReturn($select); + $select->expects($this->once())->method('joinLeft')->willReturn($select); + $select->expects($this->any())->method('where')->willReturn($select); + $select->expects($this->once())->method('useStraightJoin'); + $select->expects($this->exactly(2))->method('insertFromSelect'); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->exactly(4)) + ->method('getDatePartSql') + ->willReturn($periodExpr); + $connection->expects($this->any())->method('select')->willReturn($select); + $query = $this->createMock(\Zend_Db_Statement_Interface::class); + $connection->expects($this->exactly(3))->method('query')->willReturn($query); + $resource = $this->createMock(ResourceConnection::class); + $resource->expects($this->any()) + ->method('getConnection') + ->with($this->connectionName) + ->willReturn($connection); + $this->context->expects($this->any())->method('getResources')->willReturn($resource); + + $store = $this->createMock(StoreInterface::class); + $store->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->once())->method('getStores')->with(true)->willReturn([$store]); + + $this->helper->expects($this->exactly(3))->method('getBestsellersReportUpdateRatingPos'); + + $flag = $this->createMock(Flag::class); + $flag->expects($this->once())->method('setReportFlagCode')->willReturn($flag); + $flag->expects($this->once())->method('unsetData')->willReturn($flag); + $flag->expects($this->once())->method('loadSelf'); + $this->flagFactory->expects($this->once())->method('create')->willReturn($flag); + + $date = $this->createMock(\DateTime::class); + $date->expects($this->exactly(4))->method('format')->with('e'); + $this->time->expects($this->exactly(4))->method('scopeDate')->willReturn($date); + + $this->report = new Bestsellers( + $this->context, + $this->logger, + $this->time, + $this->flagFactory, + $this->validator, + $this->date, + $this->product, + $this->helper, + $this->connectionName, + [], + $this->storeManager + ); + + $this->report->aggregate($from, $to); + } + + /** + * @return void + * @throws \Exception + */ + public function testAggregatePerStoreCalculationNoInterval(): void + { + $periodExpr = 'DATE(DATE_ADD(`source_table`.`created_at`, INTERVAL -25200 SECOND))'; + $select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->exactly(2))->method('group'); + $select->expects($this->exactly(3))->method('from')->willReturn($select); + $select->expects($this->once())->method('joinInner')->willReturn($select); + $select->expects($this->once())->method('joinLeft')->willReturn($select); + $select->expects($this->exactly(3))->method('where')->willReturn($select); + $select->expects($this->once())->method('useStraightJoin'); + $select->expects($this->exactly(2))->method('insertFromSelect'); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->once()) + ->method('getDatePartSql') + ->willReturn($periodExpr); + $connection->expects($this->any())->method('select')->willReturn($select); + $connection->expects($this->exactly(2))->method('query'); + $resource = $this->createMock(ResourceConnection::class); + $resource->expects($this->any()) + ->method('getConnection') + ->with($this->connectionName) + ->willReturn($connection); + $this->context->expects($this->any())->method('getResources')->willReturn($resource); + + $store = $this->createMock(StoreInterface::class); + $store->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->once())->method('getStores')->with(true)->willReturn([$store]); + + $this->helper->expects($this->exactly(3))->method('getBestsellersReportUpdateRatingPos'); + + $flag = $this->createMock(Flag::class); + $flag->expects($this->once())->method('setReportFlagCode')->willReturn($flag); + $flag->expects($this->once())->method('unsetData')->willReturn($flag); + $flag->expects($this->once())->method('loadSelf'); + $this->flagFactory->expects($this->once())->method('create')->willReturn($flag); + + $date = $this->createMock(\DateTime::class); + $date->expects($this->once())->method('format')->with('e'); + $this->time->expects($this->once())->method('scopeDate')->willReturn($date); + + $this->report = new Bestsellers( + $this->context, + $this->logger, + $this->time, + $this->flagFactory, + $this->validator, + $this->date, + $this->product, + $this->helper, + $this->connectionName, + [], + $this->storeManager + ); + + $this->report->aggregate(); + } +} diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 112e927bf4c9d..3297d96c6ef7d 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -294,6 +294,11 @@ <index referenceId="SALES_ORDER_EMAIL_SENT" indexType="btree"> <column name="email_sent"/> </index> + <index referenceId="SALES_ORDER_STORE_STATE_CREATED" indexType="btree"> + <column name="store_id"/> + <column name="state"/> + <column name="created_at"/> + </index> </table> <table name="sales_order_grid" resource="sales" engine="innodb" comment="Sales Flat Order Grid"> <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index 02efd7d5a0050..664c65d36c3c7 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -142,6 +142,7 @@ "SALES_ORDER_STATUS": true, "SALES_ORDER_STATE": true, "SALES_ORDER_STORE_ID": true, + "SALES_ORDER_STORE_ID_STATE_CREATED_AT": true, "SALES_ORDER_CREATED_AT": true, "SALES_ORDER_CUSTOMER_ID": true, "SALES_ORDER_EXT_ORDER_ID": true, diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index fba7fc65870a5..aff5a8e6f7063 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -808,3 +808,5 @@ If set YES Email field will be required during Admin order creation for new Cust "This creditmemo no longer exists.","This creditmemo no longer exists." "Add to address book","Add to address book" "Logo for PDF Print-outs","Logo for PDF Print-outs" +"Please provide a comment text or update the order status to be able to submit a comment for this order.", "Please provide a comment text or update the order status to be able to submit a comment for this order." +"A status change or comment text is required to submit a comment.", "A status change or comment text is required to submit a comment." diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml index a168a89ed5ef4..cabad529c739b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml @@ -36,6 +36,13 @@ cols="5" id="history_comment" class="admin__control-textarea"></textarea> + <div class="admin__field-note"> + <span> + <?= $block->escapeHtml( + __('A status change or comment text is required to submit a comment.') + )?> + </span> + </div> </div> </div> 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 12b25abe1dec6..9c46b12c01377 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 @@ -3,7 +3,7 @@ * See COPYING.txt for license details. */ -define([ + define([ 'jquery', 'Magento_Ui/js/modal/confirm', 'Magento_Ui/js/modal/alert', @@ -157,7 +157,14 @@ define([ this.sidebarShow(); //this.loadArea(['header', 'sidebar','data'], true); this.dataShow(); - this.loadArea(['header', 'data'], true); + this.loadArea( + ['header', 'data'], + true, + null, + function () { + location.reload(); + } + ); }, setCurrencyId: function (id) { @@ -184,7 +191,7 @@ define([ } this.selectAddressEvent = false; - var data = this.serializeData(container); + let data = this.serializeData(container).toObject(); data[el.name] = id; this.resetPaymentMethod(); @@ -1163,7 +1170,7 @@ define([ } }, - loadArea: function (area, indicator, params) { + loadArea: function (area, indicator, params, callback) { var deferred = new jQuery.Deferred(); var url = this.loadBaseUrl; if (area) { @@ -1182,6 +1189,9 @@ define([ onSuccess: function (transport) { var response = transport.responseText.evalJSON(); this.loadAreaResponseHandler(response); + if (callback instanceof Function) { + callback(); + } deferred.resolve(); }.bind(this) }); diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js b/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js index a1155dd436d49..07ba2ad319487 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js @@ -25,7 +25,7 @@ define([ })); } - $('#order-view-cancel-button').on('click', function () { + $(document).on('click', '#order-view-cancel-button', function () { var msg = $.mage.__('Are you sure you want to cancel this order?'), url = $('#order-view-cancel-button').data('url'); @@ -45,13 +45,13 @@ define([ return false; }); - $('#order-view-hold-button').on('click', function () { + $(document).on('click', '#order-view-hold-button', function () { var url = $('#order-view-hold-button').data('url'); getForm(url).appendTo('body').trigger('submit'); }); - $('#order-view-unhold-button').on('click', function () { + $(document).on('click', '#order-view-unhold-button', function () { var url = $('#order-view-unhold-button').data('url'); getForm(url).appendTo('body').trigger('submit'); diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php index 3bfcbb1426c2f..1c2b8e50de2a2 100644 --- a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php @@ -14,6 +14,8 @@ use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\OrderItemRepositoryInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; /** * Data provider for order items @@ -45,6 +47,11 @@ class DataProvider */ private $optionsProcessor; + /** + * @var TaxHelper + */ + private $taxHelper; + /** * @var int[] */ @@ -61,19 +68,22 @@ class DataProvider * @param OrderRepositoryInterface $orderRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param OptionsProcessor $optionsProcessor + * @param TaxHelper|null $taxHelper */ public function __construct( OrderItemRepositoryInterface $orderItemRepository, ProductRepositoryInterface $productRepository, OrderRepositoryInterface $orderRepository, SearchCriteriaBuilder $searchCriteriaBuilder, - OptionsProcessor $optionsProcessor + OptionsProcessor $optionsProcessor, + ?TaxHelper $taxHelper = null ) { $this->orderItemRepository = $orderItemRepository; $this->productRepository = $productRepository; $this->orderRepository = $orderRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->optionsProcessor = $optionsProcessor; + $this->taxHelper = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); } /** @@ -140,7 +150,9 @@ private function fetch() 'status' => $orderItem->getStatus(), 'discounts' => $this->getDiscountDetails($associatedOrder, $orderItem), 'product_sale_price' => [ - 'value' => $orderItem->getPrice(), + 'value' => $this->taxHelper->displaySalesPriceInclTax($associatedOrder->getStoreId()) + ? $orderItem->getPriceInclTax() + : $orderItem->getPrice(), 'currency' => $associatedOrder->getOrderCurrencyCode() ], 'selected_options' => $itemOptions['selected_options'], diff --git a/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php b/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php new file mode 100644 index 0000000000000..98cf3e6706657 --- /dev/null +++ b/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Api\Data; + +interface DiscountAppliedToInterface +{ + public const APPLIED_TO_ITEM = 'ITEM'; + public const APPLIED_TO_SHIPPING = 'SHIPPING'; + public const APPLIED_TO = 'applied_to'; + /** + * Get entity type the diescount is applied to + * + * @return string + */ + public function getAppliedTo(); +} diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php index e407142e6893a..1c49b16161a5a 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php @@ -64,7 +64,7 @@ public function execute() { $data = $this->getRequest()->getPostValue(); if ($data) { - $data['simple_free_shipping'] = ($data['simple_free_shipping'] === '') + $data['simple_free_shipping'] = (($data['simple_free_shipping'] ?? '') === '') ? null : $data['simple_free_shipping']; try { @@ -93,7 +93,6 @@ public function execute() } $session = $this->_objectManager->get(\Magento\Backend\Model\Session::class); - $validateResult = $model->validateData(new \Magento\Framework\DataObject($data)); if ($validateResult !== true) { foreach ($validateResult as $errorMessage) { @@ -120,13 +119,14 @@ public function execute() $data['actions'] = $data['rule']['actions']; } unset($data['rule']); + + $data = $this->updateCouponData($data); $model->loadPost($data); $useAutoGeneration = (int)( !empty($data['use_auto_generation']) && $data['use_auto_generation'] !== 'false' ); $model->setUseAutoGeneration($useAutoGeneration); - $session->setPageData($model->getData()); $model->save(); @@ -177,4 +177,21 @@ private function checkRuleExists(\Magento\SalesRule\Model\Rule $model): bool } return true; } + + /** + * Update data related to Coupon + * + * @param array $data + * @return array + */ + private function updateCouponData(array $data): array + { + if (isset($data['uses_per_coupon']) && $data['uses_per_coupon'] === '') { + $data['uses_per_coupon'] = 0; + } + if (isset($data['uses_per_customer']) && $data['uses_per_customer'] === '') { + $data['uses_per_customer'] = 0; + } + return $data; + } } diff --git a/app/code/Magento/SalesRule/Model/Data/DiscountData.php b/app/code/Magento/SalesRule/Model/Data/DiscountData.php index cfad4b5c09c55..ab5f1f86c8a18 100644 --- a/app/code/Magento/SalesRule/Model/Data/DiscountData.php +++ b/app/code/Magento/SalesRule/Model/Data/DiscountData.php @@ -8,18 +8,21 @@ namespace Magento\SalesRule\Model\Data; use Magento\SalesRule\Api\Data\DiscountDataInterface; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface; use Magento\Framework\Api\ExtensionAttributesInterface; /** * Discount Data Model */ -class DiscountData extends \Magento\Framework\Api\AbstractExtensibleObject implements DiscountDataInterface +class DiscountData extends \Magento\Framework\Api\AbstractExtensibleObject implements + DiscountDataInterface, + DiscountAppliedToInterface { - const AMOUNT = 'amount'; - const BASE_AMOUNT = 'base_amount'; - const ORIGINAL_AMOUNT = 'original_amount'; - const BASE_ORIGINAL_AMOUNT = 'base_original_amount'; + public const AMOUNT = 'amount'; + public const BASE_AMOUNT = 'base_amount'; + public const ORIGINAL_AMOUNT = 'original_amount'; + public const BASE_ORIGINAL_AMOUNT = 'base_original_amount'; /** * Get Amount @@ -126,4 +129,14 @@ public function setExtensionAttributes( ) { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Get entity type the discount is applied to + * + * @return string + */ + public function getAppliedTo() + { + return $this->_get(DiscountAppliedToInterface::APPLIED_TO) ?: DiscountAppliedToInterface::APPLIED_TO_ITEM; + } } diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 3775bc9122cb4..62e9349985b39 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -222,6 +222,8 @@ public function collect( $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); $address->setDiscountAmount($total->getDiscountAmount()); $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + $address->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + $address->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); return $this; } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php index 42498448cd13f..c22deb659b375 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Model\ResourceModel; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTime as Date; /** * SalesRule Resource Coupon @@ -15,6 +18,33 @@ class Coupon extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements \Magento\SalesRule\Model\Spi\CouponResourceInterface { + /** + * @var Date + */ + private $date; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @param Context $context + * @param Date $date + * @param DateTime $dateTime + * @param string|null $connectionName + */ + public function __construct( + Context $context, + Date $date, + DateTime $dateTime, + $connectionName = null + ) { + parent::__construct($context, $connectionName); + $this->date = $date; + $this->dateTime = $dateTime; + } + /** * Constructor adds unique fields * @@ -37,6 +67,9 @@ public function _beforeSave(AbstractModel $object) // maintain single primary coupon per rule $object->setIsPrimary($object->getIsPrimary() ? 1 : null); + $object->setUsageLimit($object->getUsageLimit() ?? 0); + $object->setUsagePerCustomer($object->getUsagePerCustomer() ?? 0); + $object->setCreatedAt($object->getCreatedAt() ?? $this->dateTime->formatDate($this->date->gmtTimestamp())); return parent::_beforeSave($object); } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php index d65021ed82a2e..125e2194b45e0 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php @@ -312,6 +312,10 @@ public function getActiveAttributes() ['ea' => $this->getTable('eav_attribute')], 'ea.attribute_id = a.attribute_id', [] + )->joinInner( + ['sr' => $this->getTable('salesrule')], + 'a.' . $this->getLinkField() . ' = sr.' . $this->getLinkField() . ' AND sr.is_active = 1', + [] ); return $connection->fetchAll($select); } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php index a807bca77cc60..f40fe129a7618 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php @@ -5,6 +5,8 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +use Magento\Framework\Model\AbstractModel; + class Found extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -53,21 +55,34 @@ public function asHtml() /** * Validate * - * @param \Magento\Framework\Model\AbstractModel $model + * @param AbstractModel $model * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function validate(\Magento\Framework\Model\AbstractModel $model) + public function validate(AbstractModel $model) { $isValid = false; + $all = $this->getAggregator() === 'all'; + $true = (bool)$this->getValue(); foreach ($model->getAllItems() as $item) { - if (parent::validate($item)) { + $validated = parent::validate($item); + if (!$true && !$validated) { + $isValid = false; + break; + } + if (!$all && $validated) { $isValid = true; break; } + if ($all && $true && $validated) { + $isValid = true; + break; + } + if ($all && !$true && $validated) { + $isValid = true; + } } - return $isValid; } } diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index a0189888d9746..ba0ba696dbd7b 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -15,6 +15,7 @@ use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface as DiscountAppliedTo; /** * Rule applier model @@ -150,6 +151,26 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) public function addDiscountDescription($address, $rule) { $description = $address->getDiscountDescriptionArray(); + $label = $this->getRuleLabel($address, $rule); + + if (strlen($label)) { + $description[$rule->getId()] = $label; + } + + $address->setDiscountDescriptionArray($description); + + return $this; + } + + /** + * Retrieve rule label + * + * @param Address $address + * @param Rule $rule + * @return string + */ + private function getRuleLabel(Address $address, Rule $rule): string + { $ruleLabel = $rule->getStoreLabel($address->getQuote()->getStore()); $label = ''; if ($ruleLabel) { @@ -163,14 +184,30 @@ public function addDiscountDescription($address, $rule) } } } + return $label; + } - if (strlen($label)) { - $description[$rule->getId()] = $label; - } - - $address->setDiscountDescriptionArray($description); - - return $this; + /** + * Add rule shipping discount description label to address object + * + * @param Address $address + * @param Rule $rule + * @param array $discount + * @return void + */ + public function addShippingDiscountDescription(Address $address, Rule $rule, array $discount): void + { + $addressDiscounts = $address->getExtensionAttributes()->getDiscounts(); + $ruleLabel = $this->getRuleLabel($address, $rule); + $discount[DiscountAppliedTo::APPLIED_TO] = DiscountAppliedTo::APPLIED_TO_SHIPPING; + $discountData = $this->discountDataInterfaceFactory->create(['data' => $discount]); + $data = [ + 'discount' => $discountData, + 'rule' => $ruleLabel, + 'rule_id' => $rule->getRuleId(), + ]; + $addressDiscounts[] = $this->discountInterfaceFactory->create(['data' => $data]); + $address->getExtensionAttributes()->setDiscounts($addressDiscounts); } /** diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index abf34172c7296..4d020c78843b6 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -8,6 +8,7 @@ use Laminas\Validator\ValidatorInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; @@ -28,7 +29,7 @@ * @method Validator setCustomerGroupId($id) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Validator extends \Magento\Framework\Model\AbstractModel +class Validator extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * Rule source collection @@ -151,6 +152,18 @@ public function __construct( parent::__construct($context, $registry, $resource, $resourceCollection, $data); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->counter = 0; + $this->_skipActionsValidation = false; + $this->_rulesItemTotals = []; + $this->_isFirstTimeResetRun = true; + $this->_rules = null; + } + /** * Init validator * Init process load collection of rules for specific website, @@ -408,7 +421,13 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $quoteAmount; break; } - + if ($address->getShippingDiscountAmount() + $discountAmount <= $shippingAmount) { + $data = [ + 'amount' => $discountAmount, + 'base_amount' => $baseDiscountAmount + ]; + $this->rulesApplier->addShippingDiscountDescription($address, $rule, $data); + } $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount); $baseDiscountAmount = min( $address->getBaseShippingDiscountAmount() + $baseDiscountAmount, @@ -427,7 +446,6 @@ public function processShippingAmount(Address $address) $address->setAppliedRuleIds($this->validatorUtility->mergeIds($address->getAppliedRuleIds(), $appliedRuleIds)); $quote->setAppliedRuleIds($this->validatorUtility->mergeIds($quote->getAppliedRuleIds(), $appliedRuleIds)); - return $this; } diff --git a/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php index 02fd81078ea7c..07e8aed8ef288 100644 --- a/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php +++ b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php @@ -9,6 +9,8 @@ namespace Magento\SalesRule\Observer; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; use Magento\Quote\Api\CartRepositoryInterface; @@ -28,9 +30,9 @@ class CouponCodeValidation implements ObserverInterface private $cartRepository; /** - * @var SearchCriteriaBuilder + * @var SearchCriteriaBuilderFactory */ - private $criteriaBuilder; + private $criteriaBuilderFactory; /** * @var CodeLimitManagerInterface @@ -40,16 +42,20 @@ class CouponCodeValidation implements ObserverInterface /** * @param CodeLimitManagerInterface $codeLimitManager * @param CartRepositoryInterface $cartRepository - * @param SearchCriteriaBuilder $criteriaBuilder + * @param SearchCriteriaBuilder $criteriaBuilder Deprecated. Use $criteriaBuilderFactory instead + * @param SearchCriteriaBuilderFactory|null $criteriaBuilderFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( CodeLimitManagerInterface $codeLimitManager, CartRepositoryInterface $cartRepository, - SearchCriteriaBuilder $criteriaBuilder + SearchCriteriaBuilder $criteriaBuilder, + SearchCriteriaBuilderFactory $criteriaBuilderFactory = null ) { $this->codeLimitManager = $codeLimitManager; $this->cartRepository = $cartRepository; - $this->criteriaBuilder = $criteriaBuilder; + $this->criteriaBuilderFactory = $criteriaBuilderFactory + ?: ObjectManager::getInstance()->get(SearchCriteriaBuilderFactory::class); } /** @@ -61,10 +67,11 @@ public function execute(EventObserver $observer) $quote = $observer->getData('quote'); $code = $quote->getCouponCode(); if ($code) { + $criteriaBuilder = $this->criteriaBuilderFactory->create(); //Only validating the code if it's a new code. /** @var Quote[] $found */ $found = $this->cartRepository->getList( - $this->criteriaBuilder->addFilter('main_table.' . CartInterface::KEY_ENTITY_ID, $quote->getId()) + $criteriaBuilder->addFilter('main_table.' . CartInterface::KEY_ENTITY_ID, $quote->getId()) ->create() )->getItems(); if (!$found || ((string)array_shift($found)->getCouponCode()) !== (string)$code) { diff --git a/app/code/Magento/SalesRule/README.md b/app/code/Magento/SalesRule/README.md index 88fb4e2acd45d..1f693e18c9ecf 100644 --- a/app/code/Magento/SalesRule/README.md +++ b/app/code/Magento/SalesRule/README.md @@ -1,2 +1 @@ SalesRule module is responsible for managing and processing Promotion Shopping Cart Rules. - diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml new file mode 100644 index 0000000000000..a8ca43ad6b6ca --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup" extends="AdminCreateCartPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateCartPriceRuleActionGroup. Removes 'fillDiscountAmount'. Adds sub total excl tax conditions for free shipping to a Cart Price Rule.</description> + </annotations> + <arguments> + <argument name="ruleName"/> + </arguments> + <remove keyForRemoval="fillDiscountAmount"/> + <!-- Expand the conditions section --> + <grabTextFrom selector="{{AdminCartPriceRulesFormSection.ruleName}}" after="fillRuleName" stepKey="getSubtotalRule"/> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="openConditionsSection" after="selectActionType"/> + <click selector="#conditions__1__children>li:nth-child(1)>span:nth-child(1) a" after="openConditionsSection" stepKey="addFirstCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.conditionSelect}}" userInput="{{ruleName.condition1}}" after="addFirstCondition" stepKey="selectCondition1"/> + <waitForPageLoad after="selectCondition1" stepKey="waitForConditionLoad"/> + <click selector="{{AdminCartPriceRulesFormSection.condition(ruleName.ruleToChange1)}}" after="waitForConditionLoad" stepKey="clickToChooseOption"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.conditionsOperator}}" userInput="{{ruleName.rule1}}" after="clickToChooseOption" stepKey="setOperatorType"/> + <click selector="{{AdminCartPriceRulesFormSection.targetEllipsis}}" after="setOperatorType" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleFieldByIndex('1--1')}}" userInput="{{ruleName.subtotal}}" after="clickEllipsis" stepKey="fillSubtotalParameter"/> + <click selector="{{AdminCartPriceRulesFormSection.discardSubsequentRules}}" after="fillSubtotalParameter" stepKey="clickDiscardSubsequentRules"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.freeShipping}}" userInput="{{ruleName.simple_free_shipping}}" after="clickDiscardSubsequentRules" stepKey="selectForMatchingItemsOnly"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml index 01fdc4dd120a0..8b8491b3eb5e5 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml @@ -21,7 +21,9 @@ <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> + <waitForElementClickable selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="waitForDeleteButtonClickable" /> <click selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="clickDeleteButton"/> + <waitForElementClickable selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="waitForConfirmButtonClickable" /> <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 98b3c9b9ec969..0aee40dc7bdb7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -357,6 +357,49 @@ <data key="defaultRuleLabelAllStoreViews">Free Shipping in conditions</data> <data key="defaultStoreView">Free Shipping in conditions</data> </entity> + <entity name="CartPriceRuleFreeShippingAppliedOnly"> + <data key="name" unique="suffix">Cart Price Rule For FreeShipping Only</data> + <data key="description">Description for Cart Price Rule</data> + <data key="is_active">Yes</data> + <data key="websites">Main Website</data> + <data key="customerGroups">NOT LOGGED IN</data> + <data key="coupon_type">No Coupon</data> + <data key="simple_action">Percent of product price discount</data> + <data key="discount_amount">0</data> + <data key="maximumQtyDiscount">0</data> + <data key="discount_step">0</data> + <data key="apply">Percent of product price discount</data> + <data key="condition1">Subtotal (Excl. Tax)</data> + <data key="rule1">equals or greater than</data> + <data key="subtotal">100</data> + <data key="ruleToChange1">is</data> + <data key="apply_to_shipping">0</data> + <data key="stop_rules_processing">false</data> + <data key="simple_free_shipping">For matching items only</data> + <data key="defaultRuleLabelAllStoreViews">Free Shipping in conditions</data> + <data key="defaultStoreView">Free Shipping in conditions</data> + </entity> + <entity name="CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax"> + <data key="name" unique="suffix">Cart Price Rule For Rule Condition</data> + <data key="description">Description for Cart Price Rule</data> + <data key="is_active">Yes</data> + <data key="websites">Main Website</data> + <data key="customerGroups">NOT LOGGED IN</data> + <data key="coupon_type">Specific Coupon</data> + <data key="coupon_code" unique="suffix">123-abc-ABC-987</data> + <data key="uses_per_coupon">13</data> + <data key="uses_per_customer">63</data> + <data key="simple_action">Percent of product price discount</data> + <data key="discount_amount">10</data> + <data key="maximumQtyDiscount">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">0</data> + <data key="simple_free_shipping">No</data> + <data key="stop_rules_processing">false</data> + <data key="apply">Percent of product price discount</data> + <data key="defaultRuleLabelAllStoreViews">Free Shipping in Rule conditions</data> + <data key="defaultStoreView">Free Shipping in Rule conditions</data> + </entity> <entity name="CartPriceRuleConditionAppliedForSubtotal"> <data key="name" unique="suffix">Cart Price Rule For Rule Condition</data> <data key="description">Description for Cart Price Rule</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml index a4318103c4c00..d88759e08ffad 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml @@ -19,6 +19,7 @@ <testCaseId value="MC-42602"/> <useCaseId value="MC-42288"/> <group value="salesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml index 18b9636e62e2b..14fb4405c5030 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-96722"/> <useCaseId value="MAGETWO-96410"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml index c65aa9980666f..59ee88b2feb8b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml @@ -16,6 +16,7 @@ <description value="Use cart price rule of type Buy X get Y free with enable 'Apply to Shipping Amount'"/> <severity value="MAJOR"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <remove keyForRemoval="verifyStorefront"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml index 25d9d431d1c51..428d149d01cf7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="SalesRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index 65bb0b4cbfb99..805bb3e1a8397 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-5299"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml index cd72ec8529816..7168fa6a3ee0a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="SalesRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index fab4c79da6286..a26002bbf9edc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-91"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml index 83648cec149d0..06c585250b23a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml @@ -16,6 +16,7 @@ <description value="Admin can not create rule with invalid data"/> <severity value="MAJOR"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml index 3aacc176acdc5..3b96f89db7e16 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="salesRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml index 8c02f401992ee..279e8e5677a85 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="salesRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml index 18183085060d2..8ec440b494ee3 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="salesRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml index d58912c58937a..cf225add6e821 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml index 89e50d51d1efd..a06b9e66fab06 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml @@ -16,6 +16,7 @@ <useCaseId value="ACP2E-1053"/> <testCaseId value="AC-6642"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="SalesRuleSpecificCoupon" stepKey="createSalesRule"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index 94c372c0eef77..6fc0c733cd53b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94471"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> @@ -82,15 +83,16 @@ </before> <after> - <actionGroup ref="DeleteCartPriceRuleByName" stepKey="DeleteCartPriceRuleByName"> - <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> - </actionGroup> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="DeleteCartPriceRuleByName"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> @@ -107,7 +109,7 @@ <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> - <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ABCD" stepKey="fillCouponCOde"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCOde"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="50" stepKey="fillDiscountAmount"/> <scrollTo selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="ScrollToApplyRuleForConditions"/> @@ -137,7 +139,7 @@ <!--View and edit cart--> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickViewAndEditCartFromMiniCart"/> <click selector="{{DiscountSection.DiscountTab}}" stepKey="scrollToDiscountTab" /> - <fillField selector="{{DiscountSection.CouponInput}}" userInput="ABCD" stepKey="fillCouponCode" /> + <fillField selector="{{DiscountSection.CouponInput}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCode" /> <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> <waitForPageLoad stepKey="waitForPageLoad3"/> <see userInput="You used coupon code" stepKey="assertText"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml index 2e31c7058f2b6..ac54224095fb8 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml @@ -14,6 +14,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-91101"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="subcategory1"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml index 2b2d74e2a01d2..c749756770eca 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml @@ -15,6 +15,7 @@ <description value="Test the simple free shipping options as default it should select Please select option "/> <severity value="MAJOR"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml index 9bcefdcfc3144..bc070ff75cae6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml @@ -18,6 +18,7 @@ <testCaseId value="AC-1618"/> <useCaseId value="ACP2E-285"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml index 80eb79d9cc6f0..3679c180328bc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml @@ -19,6 +19,7 @@ <group value="salesRule"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct1"> <field key="price">5.00</field> </createData> @@ -112,6 +113,7 @@ <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> <waitForPageLoad stepKey="waitForDetailsOpen"/> <!--Check all products and Cart Subtotal and Discount is only for SimpleProduct1--> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection" /> <actionGroup ref="StorefrontCheckCartTotalWithDiscountCategoryActionGroup" stepKey="checkDiscountIsAppliedOnlyForSimple1productOnly"> <argument name="subtotal" value="12.00"/> <argument name="shipping" value="5.00"/> @@ -136,6 +138,7 @@ <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart2"/> <waitForPageLoad stepKey="waitForDetailsOpen2"/> <!--Check all products and Cart Subtotal and Discount is only for SimpleProduct2--> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSectionForSimple2" /> <actionGroup ref="StorefrontCheckCartTotalWithDiscountCategoryActionGroup" stepKey="checkDiscountIsAppliedOnlyForSimple2productOnly"> <argument name="subtotal" value="10.00"/> <argument name="shipping" value="5.00"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index a5221c3668dbb..93db674608860 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -31,7 +31,7 @@ <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> </actionGroup> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 190703b25a889..c6308721f183d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -31,7 +31,7 @@ <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> </actionGroup> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index 5d51ee02ffe2f..815484ee6b6d7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -31,7 +31,7 @@ <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> </actionGroup> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 3583cc7c1cf19..15e82f81d5b77 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-235"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml index a8729ccd40f6f..a93ea8f026a67 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml @@ -38,7 +38,7 @@ <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromStorefront"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteSalesRule"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -60,7 +60,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/> <!-- Go to Order review --> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToCheckoutReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutReview"/> <!-- Apply Discount Coupon to the Order --> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml index c039fcad311a1..45a06bc59f101 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-70192"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> <!-- Create two Categories: CAT1 and CAT2 --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml index 3e86cdf17767d..27428b26b15b0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml @@ -16,8 +16,12 @@ <severity value="CRITICAL"/> <testCaseId value="MC-31863"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <createData entity="ApiCategory" stepKey="createCategoryOne"/> <createData entity="ApiSimpleProduct" stepKey="createFirstSimpleProduct"> <field key ="price">100</field> @@ -78,6 +82,9 @@ </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Start to create new cart price rule via Category conditions --> <actionGroup ref="AdminCreateCartPriceRuleWithConditionIsCategoryActionGroup" stepKey="createCartPriceRuleWithCondition"> @@ -107,9 +114,10 @@ </actionGroup> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="openTheCartWithFirstAndSecondGroupedProducts"/> <waitForPageLoad stepKey="waitForGrandTotalToLoad"/> - <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> - <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalElement"/> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection" /> <!-- Discount amount is applied for product from first category only --> <actionGroup ref="StorefrontCheckCartTotalWithDiscountCategoryActionGroup" stepKey="checkDiscountIsApplied"> <argument name="subtotal" value="$1,000.00"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml index 6e3ca918adfd4..d7880e64e5681 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml @@ -87,7 +87,7 @@ <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="clickProceedToCheckout"/> <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" stepKey="waitForCheckoutPage"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml index 2a989f3d0e54c..0508c246a9428 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-42802"/> <useCaseId value="MC-42612"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 82ca394effff5..7e3c028d912ae 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -110,7 +110,7 @@ protected function setUp(): void $this->helper = new ObjectManager($this); $this->rulesApplier = $this->createPartialMock( RulesApplier::class, - ['setAppliedRuleIds', 'applyRules', 'addDiscountDescription'] + ['setAppliedRuleIds', 'applyRules', 'addDiscountDescription', 'addShippingDiscountDescription'] ); $this->addressMock = $this->getMockBuilder(Address::class) diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php index b080842df447b..58e4cf42c02c0 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; use Magento\Framework\DataObject; use Magento\Framework\Event\Observer; use Magento\Quote\Api\CartRepositoryInterface; @@ -27,22 +28,27 @@ class CouponCodeValidationTest extends TestCase private $couponCodeValidation; /** - * @var MockObject|CodeLimitManagerInterface + * @var MockObject&CodeLimitManagerInterface */ private $codeLimitManagerMock; /** - * @var MockObject|CartRepositoryInterface + * @var MockObject&CartRepositoryInterface */ private $cartRepositoryMock; /** - * @var MockObject|SearchCriteriaBuilder + * @var MockObject&SearchCriteriaBuilder */ private $searchCriteriaBuilderMock; /** - * @var MockObject|Observer + * @var MockObject&SearchCriteriaBuilderFactory + */ + private $searchCriteriaBuilderMockFactory; + + /** + * @var MockObject&Observer */ private $observerMock; @@ -74,6 +80,12 @@ protected function setUp(): void ->setMethods(['addFilter', 'create']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMockFactory = $this->getMockBuilder(SearchCriteriaBuilderFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMockFactory->expects($this->any())->method('create') + ->willReturn($this->searchCriteriaBuilderMock); $this->quoteMock = $this->getMockBuilder(Quote::class) ->addMethods(['getCouponCode', 'setCouponCode']) ->onlyMethods(['getId']) @@ -83,7 +95,8 @@ protected function setUp(): void $this->couponCodeValidation = new CouponCodeValidation( $this->codeLimitManagerMock, $this->cartRepositoryMock, - $this->searchCriteriaBuilderMock + $this->searchCriteriaBuilderMock, + $this->searchCriteriaBuilderMockFactory ); } diff --git a/app/code/Magento/SalesSequence/README.md b/app/code/Magento/SalesSequence/README.md index ab34b8a233e2c..4cea80e4334d6 100644 --- a/app/code/Magento/SalesSequence/README.md +++ b/app/code/Magento/SalesSequence/README.md @@ -1,8 +1,10 @@ # Overview + ## Purpose of module Magento\SalesSequence module is responsible for sequences processing in Sales module, Magento\SalesSequence module manages sequences for next system entities and flows: + * order; * invoice; * shipment; @@ -10,9 +12,11 @@ Magento\SalesSequence module manages sequences for next system entities and flow Magento\SalesSequence module is required for Magento\Sales module. # Deployment + ## System requirements The Magento_SalesSequence module does not have any specific system requirements. ## Install + The Magento_SalesSequence module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index 3e66c2cc1c7b0..569a5a3eb9711 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -23,7 +23,8 @@ To deploy sample data from the Magento composer repository using Magento CLI: To deploy sample data from the Magento composer repository without Magento CLI: 1. Specify sample data packages in the `require` section of the root `composer.json` file, for example: -``` + +```json { "require": { ... diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml index c1c9636ca149e..4ca8e4060f8d8 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6421"/> <group value="Search"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml index 88e459178edbc..55c58b0d27c52 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14767"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create three search term --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml index 090fc1d3cb50c..5f02ede419d34 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml @@ -8,7 +8,6 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest"> - <annotations> <stories value="Search Terms"/> <title value="In this test-case we need to verify that previously used earlier search terms are auto-complete"/> @@ -22,7 +21,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> @@ -68,6 +69,5 @@ <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductNameSku"> <argument name="productName" value="$$simpleProduct.name$$"/> </actionGroup> - </test> </tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml b/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml index b46bee121044a..b382d73976d60 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml @@ -15,6 +15,9 @@ <description value="Product can be found by value of 'Searchable' attribute"/> <severity value="MAJOR"/> <testCaseId value="AC-4086"/> + <skip> + <issueId value="ACQE-4825"/> + </skip> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml index 556765cd69a78..1a55cd8085e9c 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-40466"/> <useCaseId value="MC-40376"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 14be6c7c66aab..addaaa138688b 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14765"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index be22ed0872bdc..faf923af6ae7c 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14763"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index 6dc07a6ea8686..dac8b8aef1d9a 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14766"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index 42d402a8ace82..ceeec0328c4cc 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14764"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml index 4f8cd9da856ca..9638d187c9f1e 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml @@ -14,6 +14,7 @@ <title value="Create Search Term Entity With Redirect. Check How Redirect is Working on Storefront"/> <description value="Storefront search by created search term with redirect. Verifying if created redirect is working"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml b/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml new file mode 100644 index 0000000000000..caee0bd9f0287 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest"> + <annotations> + <stories value="Search Term"/> + <title value="Use Layered Navigation to filter Products by Out of Stock option of configurable product"/> + <description value="Use Layered Navigation to filter Products by Out of Stock option of configurable product"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5228"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"> + <field key="name">Test Out Of Stock Filter</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">Product Simple 1</field> + <field key="price">200</field> + <field key="quantity">100</field> + </createData> + <createData entity="ConfigurableProductWithAttributeSet" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">Product Configurable 1</field> + <field key="price">300</field> + <field key="quantity">500</field> + </createData> + <!-- Create product attribute with 3 options --> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> + <!-- Set attribute properties --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="Test Out Of Stock Attribute" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Dropdown" stepKey="fillInputType"/> + <!-- Set advanced attribute properties --> + <click selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}" stepKey="showAdvancedAttributePropertiesSection"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" stepKey="waitForSlideOut"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="TestOutOfStockAttribute" stepKey="fillAttributeCode"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <!-- Add new attribute options --> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption1"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('1')}}" userInput="one" stepKey="fillAdminValue1"/> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption2"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" userInput="two" stepKey="fillAdminValue2"/> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption3"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('3')}}" userInput="three" stepKey="fillAdminValue3"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForGridPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!-- Filter created Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="clickOnAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <!--Assign Attribute to the Group and save the attribute set --> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttribute"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="TestOutOfStockAttribute"/> + </actionGroup> + <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToSave"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openSimpleProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField('TestOutOfStockAttribute')}}" userInput="two" stepKey="setFirstAttributeValue"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstProduct"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openConfigurableProductEditPage"> + <argument name="productId" value="$createConfigurableProduct.id$"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <actionGroup ref="AdminSelectAttributeInConfigurableAttributesGrid" stepKey="checkSecondAttribute"> + <argument name="attributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <waitForPageLoad stepKey="waitForStepLoad"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickSecondNextStep"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantityToEachSkus}}" stepKey="clickOnApplyUniqueQuantitiesToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectQuantity}}" userInput="Test Out Of Stock Attribute" stepKey="selectOption"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('one')}}" userInput="200" stepKey="enterAttributeQuantity1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('two')}}" userInput="0" stepKey="enterAttributeQuantity2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('three')}}" userInput="600" stepKey="enterAttributeQuantity3"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitThirdNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickThirdStep"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitGenerateConfigurationsButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickToGenerateConfigurations"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveButton"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + </before> + <after> + <!-- Delete all created data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProducts"/> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteSecondProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductAttributesFilter"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="openCategoryPage"> + <argument name="categoryUrl" value="$$createCategory.custom_attributes[url_key]$$"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createProduct.name$)}}" stepKey="seeSimpleProductInCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createConfigurableProduct.name$)}}" stepKey="seeConfigurableProductInCategoryPage"/> + <!-- Verify the Layered Navigation first option tab --> + <click selector="{{StorefrontLayeredNavigationSection.shoppingOptionsByName('Test Out Of Stock Attribute')}}" stepKey="clickTheAttributeFromShoppingOptions"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('one')}}" stepKey="verifyFirstOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('one','1')}}" stepKey="verifyFirstOptionNameCount"/> + <!-- second option --> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('two')}}" stepKey="verifySecondOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('two','1')}}" stepKey="verifySecondOptionNameCount"/> + <!-- third option --> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('three')}}" stepKey="verifyThirdOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('three','1')}}" stepKey="verifyThirdOptionNameCount"/> + <!-- Click on the attribute --> + <click selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('two')}}" stepKey="clickOnSecondAttributeValue"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createProduct.name$)}}" stepKey="seeSimpleProductInPage"/> + </test> +</tests> + diff --git a/app/code/Magento/Security/README.md b/app/code/Magento/Security/README.md index 76ece8057edc7..2522ef72d5435 100644 --- a/app/code/Magento/Security/README.md +++ b/app/code/Magento/Security/README.md @@ -2,6 +2,7 @@ **Security** management module _Main features:_ + 1. Added support for simultaneous admin user logins with ability to enable/disable the feature, review and disconnect the list of current logged in sessions 2. Added password complexity configuration 3. Enhanced security to prevent account takeover for sessions opened on public computers and similar: diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml index f901acb8cae6f..e92c9e5adc96c 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml index 844fc0a41c7b5..a16d708eff73e 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml index 51c92e21e4762..5d83e2a279477 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Log in to Admin Panel --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml index 90c0864c29aae..09b0d4efb7977 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml @@ -19,6 +19,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml index 00c123aebdc01..e00a8b6c976cc 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml @@ -19,6 +19,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml index ad1118fd725d3..f65d5887111b3 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml index 70d08e3622f9d..d839c435f0eca 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to change old password"/> <severity value="MAJOR"/> <testCaseId value="MC-27477"/> + <group value="cloud"/> </annotations> <before> <createData entity="AdminConstantUserNameUpdatedPassword" stepKey="createUser"/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml index d7151aff22fa7..cfd7e4d607381 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <!-- Go to storefront home page --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml index 07f8ab82822a7..2222bbf33407c 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!-- Go to storefront home page --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml index 7f6e57322fa30..94c92c2ad9f4c 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml index 37ad7e0048f37..832fcb3576666 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml index 12757ffea8636..81c27ae2c27f5 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml index 1ffd970bc14da..35efb02a0243e 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml index ff2806db473f9..b6c1fdefebb79 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml index 6e866893fa51e..364bceb242d9d 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml @@ -19,6 +19,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml index c86b6ccefa3f0..b41d1f709e7b8 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml @@ -16,5 +16,10 @@ <element name="dropdownState" type="checkbox" selector="#row_shipping_origin_region_id select"/> <element name="systemValuePostcode" type="checkbox" selector="#row_shipping_origin_postcode input[type='checkbox']"/> <element name="PostcodeValue" type="input" selector="#row_shipping_origin_postcode input[type='text']"/> + <element name="systemValueShippingPolicy" type="checkbox" selector="#row_shipping_shipping_policy_enable_shipping_policy input[type='checkbox']"/> + <element name="shippingPolicyParameters" type="block" selector="#shipping_shipping_policy-head" timeout="30"/> + <element name="shippingPolicyParametersOpened" type="block" selector="#shipping_shipping_policy-head.open" timeout="30"/> + <element name="shippingPolicy" type="block" selector="#row_shipping_shipping_policy_shipping_policy_content"/> + <element name="dropdownShippingPolicy" type="select" selector="#row_shipping_shipping_policy_enable_shipping_policy select"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index 0c0372850a3c4..c776fb6fca270 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11158"/> <useCaseId value="MAGETWO-96428"/> <group value="configuration"/> + <group value="config_dump"/> </annotations> <before> <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml new file mode 100644 index 0000000000000..6b1660ba66989 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckShippingPolicyParamsInDifferentScopesTest"> + <annotations> + <features value="Shipping"/> + <stories value="Shipping Policy Parameters"/> + <title value="Displaying of Shipping Policy Parameters in different scopes"/> + <description value="Displaying of Shipping Policy Parameters in different scopes"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7003"/> + <group value="shipping"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminShippingSettingsPage.url}}" stepKey="goToAdminShippingPage"/> + <waitForPageLoad stepKey="waitForShippingConfigLoad"/> + <conditionalClick selector="{{AdminShippingSettingsConfigSection.shippingPolicyParameters}}" dependentSelector="{{AdminShippingSettingsConfigSection.shippingPolicyParametersOpened}}" visible="false" stepKey="openShippingPolicySettings"/> + <uncheckOption selector="{{AdminShippingSettingsConfigSection.systemValueShippingPolicy}}" stepKey="disableUseDefaultCondition"/> + <selectOption selector="{{AdminShippingSettingsConfigSection.dropdownShippingPolicy}}" userInput="Yes" stepKey="SelectApplyCustomShippingPolicy"/> + + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveChanges"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchNewStoreView"> + <argument name="storeViewName" value="{{_defaultStore.name}}"/> + </actionGroup> + <seeElement selector="{{AdminShippingSettingsConfigSection.shippingPolicy}}" stepKey="seeShippingPolicy"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchtoDefaultConfig"> + <argument name="storeViewName" value="Default Config"/> + </actionGroup> + + <selectOption selector="{{AdminShippingSettingsConfigSection.dropdownShippingPolicy}}" userInput="No" stepKey="SelectApplyCustomShippingPolicy1"/> + + <checkOption selector="{{AdminShippingSettingsConfigSection.systemValueShippingPolicy}}" stepKey="enableUseDefaultCondition"/> + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveChanges1"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchNewStoreView2"> + <argument name="storeViewName" value="{{_defaultStore.name}}"/> + </actionGroup> + <seeElement selector="{{AdminShippingSettingsConfigSection.shippingPolicy}}" stepKey="seeShippingPolicy2"/> + + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml index 47727f19bef0e..d916da6e88898 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml @@ -54,7 +54,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Open new order page from admin and add product--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$testProduct$$"/> </actionGroup> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml index acbe29dd14e76..efd7ade97784f 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -90,8 +90,10 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Create order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml index b1fb2aad54272..7a90f63d27831 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml @@ -42,7 +42,7 @@ <!-- TEST BODY --> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml index 5d46ef0a76263..efe530f4f30fa 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14330"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -43,7 +44,7 @@ <!-- TEST BODY --> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml index d60dca08e6813..7d8492301c3ee 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-39502"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml index 6a2d1d6c33a6d..a6f6fc1182648 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="shipping"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml index 52fefbe2cf751..ee71c93d48b58 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml @@ -16,6 +16,7 @@ <group value="sales"/> <testCaseId value="MC-42591" /> <useCaseId value="MC-41545"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml new file mode 100644 index 0000000000000..f973b8f700b4c --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.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="MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest"> + <annotations> + <stories value="Multi shipping with creation new customer and addresses during checkout"/> + <title value="Verify Multi shipping with creation new customer and addresses during checkout"/> + <description value="Verify Multi shipping with creation new customer and addresses during checkout"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4685" /> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">simple product</field> + </createData> + <!-- Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">config product</field> + </createData> + <!-- Search for the Created Configurable Product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openConfigurableProductEditPage"> + <argument name="productId" value="$createConfigProduct.id$"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <waitForPageLoad stepKey="waitForSelectAttributesPage"/> + <actionGroup ref="CreateOptionsForAttributeActionGroup" stepKey="createOptions"> + <argument name="attributeName" value="Color"/> + <argument name="firstOptionName" value="Red"/> + <argument name="secondOptionName" value="Green"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteAllProducts"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete customer --> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorAttributeActionGroup" stepKey="deleteRedColorAttribute"> + <argument name="Color" value="Red"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorAttributeActionGroup" stepKey="deleteBlueColorAttribute"> + <argument name="Color" value="Green"/> + </actionGroup> + <!-- reindex and flush cache --> + <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="openCategoryPage"> + <argument name="categoryUrl" value="$$createCategory.custom_attributes[url_key]$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + <!-- Add configurable product to the cart --> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart1"> + <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> + <argument name="productAttribute" value="Color"/> + <argument name="productOption" value="Red"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart2"> + <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> + <argument name="productAttribute" value="Color"/> + <argument name="productOption" value="Green"/> + <argument name="qty" value="1"/> + </actionGroup> + <!-- Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <waitForElementVisible selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="waitMultipleAddressShippingButton"/> + <click selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="clickToMultipleAddressShippingButton"/> + <!--Create an account--> + <waitForElementVisible selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="waitCreateAnAccountButton"/> + <click selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="clickOnCreateAnAccountButton"/> + <waitForPageLoad stepKey="waitForCreateAccountPageToLoad"/> + <actionGroup ref="EnterAddressDetailsActionGroup" stepKey="enterAddressInfo"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + <actionGroup ref="StorefrontFillCustomerCreateAnAccountActionGroup" stepKey="fillDetails"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <click selector="{{MultishippingSection.enterNewAddress}}" stepKey="clickOnAddress"/> + <waitForPageLoad stepKey="waitForAddressFieldsPageOpen"/> + <actionGroup ref="FillNewCustomerAddressFieldsActionGroup" stepKey="editAddressFields"> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + </actionGroup> + <actionGroup ref="StorefrontSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForShippingPageToOpen"/> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddressForSecondProduct"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectGEAddress"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, Berlin 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontChangeMultishippingItemQtyActionGroup" stepKey="setProductQuantity"> + <argument name="sequenceNumber" value="3"/> + <argument name="quantity" value="10"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <waitForPageLoad stepKey="waitForPageToLoadProperly"/> + <seeElement selector="{{ShippingMethodSection.productDetails('simple product','1')}}" stepKey="assertSimpleProductDetails"/> + <seeElement selector="{{ShippingMethodSection.productDetails('config product','1')}}" stepKey="assertConfigProductRedDetails"/> + <seeElement selector="{{ShippingMethodSection.productDetails('config product','10')}}" stepKey="assertConfigProductGreenDetails"/> + <!-- 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"/> + <waitForPageLoad stepKey="waitForOrderPlace"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <!-- Go to My Account > My Orders and verify orderId--> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml new file mode 100644 index 0000000000000..8ff3f81e13ff5 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.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="MultipleAddressCheckoutWithTwoDifferentRatesTest"> + <annotations> + <stories value="Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <title value="Verify Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <description value="Verify Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4499" /> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_CA_NY_Addresses" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> + </before> + <after> + <!-- Delete created data --> + <!-- disable table rate meth0d --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="disableTableRatesShippingMethod"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <!-- Make sure you have Condition Weight vs. Destination --> + <see selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRatesWeightVSDestination.condition}}" stepKey="seeDefaultCondition"/> + <!-- Import file and save config --> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTab"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="table_rate_30895.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Add product to the shopping cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCartAgain"> + <argument name="quantity" value="2"/> + </actionGroup> + <!-- Open the shopping cart page --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> + <click selector="{{MultishippingSection.checkoutWithMultipleAddresses}}" stepKey="proceedMultishipping"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectNYAddress"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, 368 Broadway St. Apt. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <see selector="{{ShippingMethodSection.shippingMethod('1','2')}}" userInput="Table Rate $5.00" stepKey="assertTableRateForLA"/> + <see selector="{{ShippingMethodSection.shippingMethod('2','2')}}" userInput="Table Rate $10.00" stepKey="assertTableRateForNY"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml index 53e91fbdb24c1..763860a11645a 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-24379"/> <group value="shipping"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index c174517375779..6bd7b66350b3d 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-6405"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml index 0f2f7ed26f1e1..b6c023e74bede 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13581"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <!-- Create product --> diff --git a/app/code/Magento/Shipping/etc/adminhtml/system.xml b/app/code/Magento/Shipping/etc/adminhtml/system.xml index 29862bdcfc8b1..a6611a2792b89 100644 --- a/app/code/Magento/Shipping/etc/adminhtml/system.xml +++ b/app/code/Magento/Shipping/etc/adminhtml/system.xml @@ -42,9 +42,6 @@ </field> <field id="shipping_policy_content" translate="label" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Shipping Policy</label> - <depends> - <field id="enable_shipping_policy">1</field> - </depends> </field> </group> </section> diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index 560797cfc7453..a29c5413335c9 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -51,7 +51,7 @@ public function __construct( */ public function execute() { - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::PUB); // check if we know what should be deleted $id = $this->getRequest()->getParam('sitemap_id'); if ($id) { diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 1543fc8df933c..a24e507cab357 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php @@ -28,7 +28,7 @@ class Save extends Sitemap implements HttpPostActionInterface /** * Maximum length of sitemap filename */ - const MAX_FILENAME_LENGTH = 32; + public const MAX_FILENAME_LENGTH = 32; /** * @var StringLength @@ -128,7 +128,7 @@ protected function validatePath(array $data) protected function clearSiteMap(\Magento\Sitemap\Model\Sitemap $model) { /** @var Filesystem $directory */ - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::PUB); if ($this->getRequest()->getParam('sitemap_id')) { $model->load($this->getRequest()->getParam('sitemap_id')); diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php index 92cbcbd500e8a..50c173ab97e82 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php @@ -1,14 +1,17 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Model\ResourceModel\Cms; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\GetUtilityPageIdentifiersInterface; use Magento\Cms\Model\Page as CmsPage; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\EntityManager\MetadataPool; @@ -21,6 +24,8 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * @SuppressWarnings(PHPMD.LongVariable) */ class Page extends AbstractDb { @@ -85,6 +90,7 @@ public function getConnection() * Retrieve cms page collection array * * @param int $storeId + * * @return array */ public function getCollection($storeId) @@ -103,7 +109,17 @@ public function getCollection($storeId) 'main_table.is_active = 1' )->where( 'main_table.identifier NOT IN (?)', - $this->getUtilityPageIdentifiers->execute() + array_map( + // When two CMS pages have the same URL key (in different + // stores), the value stored in configuration is 'url-key|ID'. + // This function strips the trailing '|ID' so that this where() + // matches the url-key configured. + // See https://github.com/magento/magento2/issues/35001 + static function ($urlKey) { + return explode('|', $urlKey, 2)[0]; + }, + $this->getUtilityPageIdentifiers->execute() + ) )->where( 'store_table.store_id IN(?)', [0, $storeId] @@ -123,11 +139,12 @@ public function getCollection($storeId) * Prepare page object * * @param array $data - * @return \Magento\Framework\DataObject + * + * @return DataObject */ protected function _prepareObject(array $data) { - $object = new \Magento\Framework\DataObject(); + $object = new DataObject(); $object->setId($data[$this->getIdFieldName()]); $object->setUrl($data['url']); $object->setUpdatedAt($data['updated_at']); @@ -140,7 +157,8 @@ protected function _prepareObject(array $data) * * @param CmsPage|AbstractModel $object * @param mixed $value - * @param string $field field to load by (defaults to model id) + * @param string $field Field to load by (defaults to model id). + * * @return $this * @since 100.1.0 */ @@ -168,6 +186,7 @@ public function load(AbstractModel $object, $value, $field = null) if ($isId) { $this->entityManager->load($object, $value); } + return $this; } @@ -190,6 +209,7 @@ public function save(AbstractModel $object) $object->setHasDataChanges(false); return $this; } + $object->validateBeforeSave(); $object->beforeSave(); if ($object->isSaveAllowed()) { @@ -201,6 +221,7 @@ public function save(AbstractModel $object) $this->unserializeFields($object); $this->processAfterSaves($object); } + $this->addCommitCallback([$object, 'afterCommitCallback'])->commit(); $object->setHasDataChanges(false); } catch (\Exception $e) { @@ -208,6 +229,7 @@ public function save(AbstractModel $object) $object->setHasDataChanges(true); throw $e; } + return $this; } diff --git a/app/code/Magento/Sitemap/README.md b/app/code/Magento/Sitemap/README.md index 1bca90e32eaad..fe2d0bb17d894 100644 --- a/app/code/Magento/Sitemap/README.md +++ b/app/code/Magento/Sitemap/README.md @@ -1,2 +1,2 @@ The Sitemap module allows managing the Magento application sitemap and -[sitemap.xml](http://en.wikipedia.org/wiki/Sitemaps) for searching engines. \ No newline at end of file +[sitemap.xml](http://en.wikipedia.org/wiki/Sitemaps) for searching engines. diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml index 4baa3c2e7d54b..a9b0ef9a8ca91 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="sitemap"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml index c61e08e25593f..f992724d34350 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="sitemap"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml index 1ac6227dfc4d6..5ccdfc683e0b8 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml index 4125f3c82e6ec..1220495021ab5 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml @@ -15,6 +15,7 @@ <title value="Sitemap use canonical for product url"/> <description value="RSS Feed always use canonical url for product"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set catalog/seo/product_use_categories 1" stepKey="enableUseCategoryPathForProductUrl"/> diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php new file mode 100644 index 0000000000000..9bcc3c0ffad77 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sitemap\Test\Unit\Controller\Adminhtml\Sitemap; + +use Magento\Backend\App\Action\Context; +use Magento\Backend\Helper\Data; +use Magento\Backend\Model\Session; +use Magento\Framework\App\ActionFlag; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\HTTP\PhpEnvironment\Request; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete; +use Magento\Sitemap\Model\SitemapFactory; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DeleteTest extends TestCase +{ + /** + * @var Context + */ + private $contextMock; + + /** + * @var Request + */ + private $requestMock; + + /** + * @var ObjectManagerInterface + */ + private $objectManagerMock; + + /** + * @var ManagerInterface + */ + private $messageManagerMock; + + /** + * @var Filesystem + */ + private $fileSystem; + + /** + * @var SitemapFactory + */ + private $siteMapFactory; + + /** + * @var Delete + */ + private $deleteController; + + /** + * @var Session + */ + private $sessionMock; + + /** + * @var ActionFlag + */ + private $actionFlag; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ResponseInterface + */ + private $response; + + /** + * @var Data + */ + private $helperMock; + + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getParam']) + ->getMockForAbstractClass(); + $this->sessionMock = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->addMethods(['setIsUrlNotice']) + ->getMock(); + $this->response = $this->getMockBuilder(ResponseInterface::class) + ->addMethods(['setRedirect']) + ->onlyMethods(['sendResponse']) + ->getMockForAbstractClass(); + $this->response->expects($this->once())->method('setRedirect'); + $this->sessionMock->expects($this->any())->method('setIsUrlNotice')->willReturn($this->objectManager); + $this->actionFlag = $this->createPartialMock(ActionFlag::class, ['get']); + $this->actionFlag->expects($this->any())->method("get")->willReturn($this->objectManager); + $this->objectManager = $this->getMockBuilder(ObjectManager::class) + ->addMethods(['get']) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + ->getMock(); + $this->helperMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->onlyMethods(['getUrl']) + ->getMock(); + $this->helperMock->expects($this->any()) + ->method('getUrl') + ->willReturn('adminhtml/*/'); + $this->contextMock->expects($this->any()) + ->method('getSession') + ->willReturn($this->sessionMock); + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManagerMock); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->response); + $this->contextMock->expects($this->any()) + ->method('getHelper') + ->willReturn($this->helperMock); + $this->contextMock->expects($this->any())->method("getActionFlag")->willReturn($this->actionFlag); + $this->fileSystem = $this->createMock(Filesystem::class); + $this->siteMapFactory = $this->createMock(SitemapFactory::class); + $this->deleteController = new Delete( + $this->contextMock, + $this->siteMapFactory, + $this->fileSystem + ); + } + + public function testDelete() + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->willReturn(null); + + $this->messageManagerMock->expects($this->never()) + ->method('addSuccessMessage'); + $this->deleteController->execute(); + } +} diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php index af14fde52c132..6e20f5063f899 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sitemap\Test\Unit\Model\ResourceModel\Cms; @@ -25,6 +27,8 @@ /** * Provide tests for Cms Page resource model. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.LongVariable) */ class PageTest extends TestCase { @@ -104,7 +108,11 @@ public function testGetCollection() $pageId = 'testPageId'; $url = 'testUrl'; $updatedAt = 'testUpdatedAt'; - $pageIdentifiers = ['testCmsHomePage', 'testCmsNoRoute', 'testCmsNoCookies']; + $pageIdentifiers = [ + 'testCmsHomePage|ID' => 'testCmsHomePage', + 'testCmsNoRoute' => 'testCmsNoRoute', + 'testCmsNoCookies' => 'testCmsNoCookies', + ]; $storeId = 1; $linkField = 'testLinkField'; $expectedPage = new DataObject(); @@ -147,7 +155,10 @@ public function testGetCollection() ->method('where') ->withConsecutive( [$this->identicalTo('main_table.is_active = 1')], - [$this->identicalTo('main_table.identifier NOT IN (?)'), $this->identicalTo($pageIdentifiers)], + [ + $this->identicalTo('main_table.identifier NOT IN (?)'), + $this->identicalTo(array_values($pageIdentifiers)) + ], [$this->identicalTo('store_table.store_id IN(?)'), $this->identicalTo([0, $storeId])] )->willReturnSelf(); @@ -176,7 +187,7 @@ public function testGetCollection() $this->getUtilityPageIdentifiers->expects($this->once()) ->method('execute') - ->willReturn($pageIdentifiers); + ->willReturn(array_keys($pageIdentifiers)); $this->resource->expects($this->exactly(2)) ->method('getTableName') diff --git a/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php b/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php deleted file mode 100644 index 58340c6cc35aa..0000000000000 --- a/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Store\App\FrontController\Plugin; - -use \Magento\Store\Model\StoreResolver\ReaderList; -use \Magento\Store\Model\ScopeInterface; - -/** - * Plugin to set default store for admin area. - */ -class DefaultStore -{ - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $storeManager; - - /** - * @var ReaderList - */ - protected $readerList; - - /** - * @var string - */ - protected $runMode; - - /** - * @var string - */ - protected $scopeCode; - - /** - * Initialize dependencies. - * - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param ReaderList $readerList - * @param string $runMode - * @param null $scopeCode - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - ReaderList $readerList, - $runMode = ScopeInterface::SCOPE_STORE, - $scopeCode = null - ) { - $this->runMode = $scopeCode ? $runMode : ScopeInterface::SCOPE_WEBSITE; - $this->scopeCode = $scopeCode; - $this->readerList = $readerList; - $this->storeManager = $storeManager; - } - - /** - * Set current store for admin area - * - * @param \Magento\Framework\App\FrontController $subject - * @param \Magento\Framework\App\RequestInterface $request - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeDispatch( - \Magento\Framework\App\FrontController $subject, - \Magento\Framework\App\RequestInterface $request - ) { - $reader = $this->readerList->getReader($this->runMode); - $defaultStoreId = $reader->getDefaultStoreId($this->scopeCode); - $this->storeManager->setCurrentStore($defaultStoreId); - } -} diff --git a/app/code/Magento/Store/Model/App/Emulation.php b/app/code/Magento/Store/Model/App/Emulation.php index 2f5eb2bad84a4..6d92909e64301 100644 --- a/app/code/Magento/Store/Model/App/Emulation.php +++ b/app/code/Magento/Store/Model/App/Emulation.php @@ -147,7 +147,9 @@ public function startEnvironmentEmulation( return; } - if ($storeId == $this->_storeManager->getStore()->getStoreId() && !$force) { + if (!$force + && ($storeId == $this->_storeManager->getStore()->getId() && $this->_viewDesign->getArea() === $area) + ) { return; } $this->storeCurrentEnvironmentInfo(); @@ -269,4 +271,12 @@ protected function _restoreInitialLocale( return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->stopEnvironmentEmulation(); + } } diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index 7f1e71c422251..8cc43afb874d9 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -9,8 +9,12 @@ */ namespace Magento\Store\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Store\Model\Validation\StoreValidator; + /** - * Class Group + * Store Group model class used to retrieve and format group information * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -21,9 +25,9 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements \Magento\Store\Api\Data\GroupInterface, \Magento\Framework\App\ScopeInterface { - const ENTITY = 'store_group'; + public const ENTITY = 'store_group'; - const CACHE_TAG = 'store_group'; + public const CACHE_TAG = 'store_group'; /** * @var bool @@ -101,10 +105,15 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements private $eventManager; /** - * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + * @var PoisonPillPutInterface */ private $pillPut; + /** + * @var StoreValidator + */ + private $modelValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -117,7 +126,8 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager - * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut + * @param PoisonPillPutInterface|null $pillPut + * @param StoreValidator|null $modelValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -132,7 +142,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], \Magento\Framework\Event\ManagerInterface $eventManager = null, - \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null + PoisonPillPutInterface $pillPut = null, + StoreValidator $modelValidator = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; @@ -140,7 +151,9 @@ public function __construct( $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); + ->get(PoisonPillPutInterface::class); + $this->modelValidator = $modelValidator ?: ObjectManager::getInstance() + ->get(StoreValidator::class); parent::__construct( $context, $registry, @@ -162,6 +175,14 @@ protected function _construct() $this->_init(\Magento\Store\Model\ResourceModel\Group::class); } + /** + * @inheritdoc + */ + protected function _getValidationRulesBeforeSave() + { + return $this->modelValidator; + } + /** * Load store collection and set internal data * diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index b3f19954ab075..2c1ad47693870 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -17,6 +17,7 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Filesystem; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Url\ModifierInterface; use Magento\Framework\Url\ScopeInterface as UrlScopeInterface; use Magento\Framework\UrlInterface; @@ -43,7 +44,8 @@ class Store extends AbstractExtensibleModel implements AppScopeInterface, UrlScopeInterface, IdentityInterface, - StoreInterface + StoreInterface, + ResetAfterRequestInterface { /** * Store Id key name @@ -466,7 +468,6 @@ protected function _construct() protected function _getSession() { if (!$this->_session->isSessionExists()) { - $this->_session->setName('store_' . $this->getCode()); $this->_session->start(); } return $this->_session; @@ -1282,6 +1283,7 @@ public function beforeDelete() * * @return $this * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Exception */ public function afterDelete() { @@ -1294,7 +1296,7 @@ function () use ($store) { ); parent::afterDelete(); $this->_configCacheType->clean(); - + $this->pillPut->put(); return $this; } @@ -1409,4 +1411,28 @@ public function setExtensionAttributes( ) { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_baseUrlCache = []; + $this->_configCache = null; + $this->_configCacheBaseNodes = []; + $this->_dirCache = []; + $this->_urlCache = []; + $this->_baseUrlCache = []; + } } diff --git a/app/code/Magento/Store/Model/StoreManager.php b/app/code/Magento/Store/Model/StoreManager.php index c3137150c8081..f6e7ebbc8d2fe 100644 --- a/app/code/Magento/Store/Model/StoreManager.php +++ b/app/code/Magento/Store/Model/StoreManager.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\ResourceModel\StoreWebsiteRelation; @@ -17,22 +17,23 @@ */ class StoreManager implements \Magento\Store\Model\StoreManagerInterface, - \Magento\Store\Api\StoreWebsiteRelationInterface + \Magento\Store\Api\StoreWebsiteRelationInterface, + ResetAfterRequestInterface { /** * Application run code */ - const PARAM_RUN_CODE = 'MAGE_RUN_CODE'; + public const PARAM_RUN_CODE = 'MAGE_RUN_CODE'; /** * Application run type (store|website) */ - const PARAM_RUN_TYPE = 'MAGE_RUN_TYPE'; + public const PARAM_RUN_TYPE = 'MAGE_RUN_TYPE'; /** * Whether single store mode enabled or not */ - const XML_PATH_SINGLE_STORE_MODE_ENABLED = 'general/single_store_mode/enabled'; + public const XML_PATH_SINGLE_STORE_MODE_ENABLED = 'general/single_store_mode/enabled'; /** * @var \Magento\Store\Api\StoreRepositoryInterface @@ -50,8 +51,6 @@ class StoreManager implements protected $websiteRepository; /** - * Scope config - * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $scopeConfig; @@ -304,6 +303,7 @@ protected function isSingleStoreModeEnabled() * Get Store Website Relation * * @deprecated 100.2.0 + * @see Nothing * @return StoreWebsiteRelation */ private function getStoreWebsiteRelation() @@ -318,4 +318,23 @@ public function getStoreByWebsiteId($websiteId) { return $this->getStoreWebsiteRelation()->getStoreByWebsiteId($websiteId); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return ['currentStoreId' => $this->currentStoreId]; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->currentStoreId = null; + } } diff --git a/app/code/Magento/Store/README.md b/app/code/Magento/Store/README.md index 877dd4a3cab25..f56b8c6bcdc37 100644 --- a/app/code/Magento/Store/README.md +++ b/app/code/Magento/Store/README.md @@ -1,4 +1,4 @@ The Store module provides one of the basic and major features of a content management system for e-commerce web sites by creating and managing a store for the customers to conduct online-shopping. Stores can be combined in groups, and are linked to a specific website. All store related configurations (currency, locale, scope etc.), management and -storage maintenance are covered under this module. \ No newline at end of file +storage maintenance are covered under this module. diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml new file mode 100644 index 0000000000000..75f9787ac5ac6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminIsDefaultWebsiteActionGroup"> + <annotations> + <description>Goes to the Admin Stores grid page. Select the provided Website Name and select the is default .</description> + </annotations> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" 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"/> + <click selector="{{AdminStoresGridSection.isDefaultUnCheckBox}}" stepKey="clickOnCheckBox"/> + <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveWebsite"/> + <see userInput="You saved the website." stepKey="seeSavedMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml index 14160835af3e1..afba374efe11c 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml @@ -13,6 +13,7 @@ <element name="storeViewDropdown" type="button" selector="#store-change-button"/> <element name="storeViewByName" type="button" selector="//*[contains(@class,'store-switcher-store-view')]/*[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> <element name="websiteByName" type="button" selector="//*[@class='store-switcher-website ']/a[contains(text(), '{{websiteName}}')]" timeout="30" parameterized="true"/> + <element name="checkWebsiteDisabled" type="button" selector="//*[contains(@class,'store-switcher-website disabled ')]"/> <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml index c0927884bdd2b..7376976b12a50 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml @@ -8,5 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewWebsiteActionsSection"> <element name="saveWebsite" type="button" selector="#save" timeout="120"/> + <element name="setAsDefault" type="checkbox" selector=".//*[@name='website[is_default]']" timeout="120"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml index 781cd680a6c3d..51d35f318ebf6 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml @@ -24,5 +24,6 @@ <element name="websiteName" type="text" selector="//td[@class='a-left col-website_title ']/a[contains(.,'{{websiteName}}')]" parameterized="true"/> <element name="gridCell" type="text" selector="//table[@class='data-grid']//tr[{{row}}]//td[count(//table[@class='data-grid']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> <element name="storeViewLinkInNthRow" type="text" selector="tr:nth-of-type({{row}}) > .col-store_title > a" parameterized="true"/> - </section> + <element name="isDefaultUnCheckBox" type="checkbox" selector="//input[@id='is_default']"/> + </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml index 56c7c3613ad94..4f7cfe90b6379 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml index 61b4107070466..e443ca1560789 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml index febc5396c6bc9..7f9b40c607b43 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml index 794a55929932c..9b54ac151ddc6 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml index 33e1a0ffedee7..d8e6e2a39a56a 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml index 15ff6c4ca0f79..6f27630fe50f0 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml index f2bfc7f7cea76..e65256a373a2b 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml index 0e6f62ef93e6e..657d35b355a37 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml index 5199b27f1fe5b..04fd6c49d3fd6 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-19306"/> <severity value="CRITICAL"/> <group value="store"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml index c24f82c09befd..2a6ad0f86631a 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml index 6c3b9f8fd689f..d898c03180b61 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml index d56d88b16863d..738e01a5a35b0 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml index 854c1025de5ec..991b9670a1618 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml @@ -16,6 +16,7 @@ <description value="Check 'Store View' sort order values no frontend store-switcher"/> <severity value="MINOR"/> <group value="store"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/etc/adminhtml/di.xml b/app/code/Magento/Store/etc/adminhtml/di.xml index e6e21f6ec0ae7..26fcbad0ff1bb 100644 --- a/app/code/Magento/Store/etc/adminhtml/di.xml +++ b/app/code/Magento/Store/etc/adminhtml/di.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Framework\App\FrontControllerInterface"> - <plugin name="default_store_setter" type="Magento\Store\App\FrontController\Plugin\DefaultStore" /> - </type> <type name="Magento\Framework\Notification\MessageList"> <arguments> <argument name="messages" xsi:type="array"> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 0698795753964..8ec1c8e6f1b59 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\Store\Api\Data\GroupInterface" type="Magento\Store\Model\Group"/> <preference for="Magento\Store\Api\Data\WebsiteInterface" type="Magento\Store\Model\Website"/> <preference for="Magento\Store\Api\StoreWebsiteRelationInterface" type="Magento\Store\Model\StoreManager"/> - <preference for="Magento\Store\Api\StoreResolverInterface" type="Magento\Store\Model\StoreResolver"/> + <preference for="Magento\Store\Api\StoreResolverInterface" type="Magento\Store\Model\StoreResolver\Proxy"/> <preference for="Magento\Framework\App\Request\PathInfoProcessorInterface" type="Magento\Store\App\Request\PathInfoProcessor" /> <preference for="Magento\Store\Model\StoreManagerInterface" type="Magento\Store\Model\StoreManager" /> <preference for="Magento\Framework\App\Response\RedirectInterface" type="Magento\Store\App\Response\Redirect" /> @@ -115,12 +115,6 @@ <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> </arguments> </type> - <type name="Magento\Store\App\FrontController\Plugin\DefaultStore"> - <arguments> - <argument name="runMode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_TYPE</argument> - <argument name="scopeCode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_CODE</argument> - </arguments> - </type> <virtualType name="Magento\Store\Model\ResourceModel\Group\Collection\FetchStrategy" type="Magento\Framework\Data\Collection\Db\FetchStrategy\Cache"> <arguments> <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument> diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php new file mode 100644 index 0000000000000..c6e96c90e0f78 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides currency code as a factor to use in the resolver cache key. + */ +class Currency implements GenericFactorProviderInterface +{ + private const NAME = "CURRENCY"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)$context->getExtensionAttributes()->getStore()->getCurrentCurrencyCode(); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php new file mode 100644 index 0000000000000..1a8fb6abc681a --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides store code as a factor to use in the resolver cache key. + */ +class Store implements GenericFactorProviderInterface +{ + private const NAME = "STORE"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return $context->getExtensionAttributes()->getStore()->getCode(); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php new file mode 100644 index 0000000000000..a8342d92c49a9 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\Stores; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity as StoreConfigIdentity; + +class ConfigIdentity implements IdentityInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + foreach ($resolvedData as $storeConfig) { + $ids[] = sprintf('%s_%s', StoreConfigIdentity::CACHE_TAG, $storeConfig['id']); + } + if (!empty($resolvedData)) { + $websiteId = $resolvedData[0]['website_id']; + $currentStoreGroupId = $this->getCurrentStoreGroupId($resolvedData); + $groupTag = $currentStoreGroupId ? 'group_' . $currentStoreGroupId : ''; + $ids[] = sprintf('%s_%s', StoreConfigIdentity::CACHE_TAG, 'website_' . $websiteId . $groupTag); + } + + return empty($ids) ? [] : array_merge([StoreConfigIdentity::CACHE_TAG], $ids); + } + + /** + * Return current store group id if it is certain that useCurrentGroup is true in the query + * + * @param array $resolvedData + * @return string|int|null + */ + private function getCurrentStoreGroupId(array $resolvedData) + { + $storeGroupCodes = array_unique(array_column($resolvedData, 'store_group_code')); + if (count($storeGroupCodes) == 1) { + try { + $store = $this->storeManager->getStore($resolvedData[0]['id']); + if ($store->getWebsite()->getGroupCollection()->count() != 1) { + // There are multiple store groups in the website while there is only one store group + // in the resolved data. Therefore useCurrentGroup must be true in the query + return $store->getStoreGroupId(); + } + } catch (NoSuchEntityException $e) { + // Do nothing + ; + } + } + return null; + } +} diff --git a/app/code/Magento/StoreGraphQl/Plugin/Group.php b/app/code/Magento/StoreGraphQl/Plugin/Group.php index fa5711bafa981..640ee20a71af8 100644 --- a/app/code/Magento/StoreGraphQl/Plugin/Group.php +++ b/app/code/Magento/StoreGraphQl/Plugin/Group.php @@ -10,7 +10,7 @@ use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; /** - * Store group plugin + * Store group plugin to provide identities for cache invalidation */ class Group { @@ -24,9 +24,17 @@ class Group public function afterGetIdentities(\Magento\Store\Model\Group $subject, array $result): array { $storeIds = $subject->getStoreIds(); - foreach ($storeIds as $storeId) { - $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $storeId); + if (count($storeIds) > 0) { + foreach ($storeIds as $storeId) { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $storeId); + } + $origWebsiteId = $subject->getOrigData('website_id'); + $websiteId = $subject->getWebsiteId(); + if ($origWebsiteId != $websiteId) { // Add or switch to a new website + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, 'website_' . $websiteId); + } } + return $result; } } diff --git a/app/code/Magento/StoreGraphQl/Plugin/Store.php b/app/code/Magento/StoreGraphQl/Plugin/Store.php index 139af175107a7..d400a378d43f3 100644 --- a/app/code/Magento/StoreGraphQl/Plugin/Store.php +++ b/app/code/Magento/StoreGraphQl/Plugin/Store.php @@ -10,7 +10,7 @@ use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; /** - * Store plugin + * Store plugin to provide identities for cache invalidation */ class Store { @@ -23,6 +23,40 @@ class Store */ public function afterGetIdentities(\Magento\Store\Model\Store $subject, array $result): array { - return array_merge($result, [sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $subject->getId())]); + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $subject->getId()); + + $isActive = $subject->getIsActive(); + // New active store or newly activated store or an active store switched store group + if ($isActive + && ($subject->getOrigData('is_active') !== $isActive || $this->isStoreGroupSwitched($subject)) + ) { + $websiteId = $subject->getWebsiteId(); + if ($websiteId !== null) { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, 'website_' . $websiteId); + $storeGroupId = $subject->getStoreGroupId(); + if ($storeGroupId !== null) { + $result[] = sprintf( + '%s_%s', + ConfigIdentity::CACHE_TAG, + 'website_' . $websiteId . 'group_' . $storeGroupId + ); + } + } + } + + return $result; + } + + /** + * Check whether the store group of the store is switched + * + * @param \Magento\Store\Model\Store $store + * @return bool + */ + private function isStoreGroupSwitched(\Magento\Store\Model\Store $store): bool + { + $origStoreGroupId = $store->getOrigData('group_id'); + $storeGroupId = $store->getStoreGroupId(); + return $origStoreGroupId != null && $origStoreGroupId != $storeGroupId; } } diff --git a/app/code/Magento/StoreGraphQl/Plugin/Website.php b/app/code/Magento/StoreGraphQl/Plugin/Website.php index 51825cd318af9..2361f277a45ea 100644 --- a/app/code/Magento/StoreGraphQl/Plugin/Website.php +++ b/app/code/Magento/StoreGraphQl/Plugin/Website.php @@ -10,7 +10,7 @@ use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; /** - * Website plugin + * Website plugin to provide identities for cache invalidation */ class Website { diff --git a/app/code/Magento/StoreGraphQl/composer.json b/app/code/Magento/StoreGraphQl/composer.json index f5fd98fdc4cae..c51fa91f121ef 100644 --- a/app/code/Magento/StoreGraphQl/composer.json +++ b/app/code/Magento/StoreGraphQl/composer.json @@ -7,7 +7,8 @@ "magento/framework": "*", "magento/module-store": "*", "magento/module-graph-ql": "*", - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index 8e82cf8141a02..8d01dd911961f 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -41,4 +41,12 @@ </argument> </arguments> </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator"> + <arguments> + <argument name="factorProviders" xsi:type="array"> + <item name="currency" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Currency</item> + <item name="store" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/module.xml b/app/code/Magento/StoreGraphQl/etc/module.xml index bbec6a85a1a13..5d41698adb056 100644 --- a/app/code/Magento/StoreGraphQl/etc/module.xml +++ b/app/code/Magento/StoreGraphQl/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Store"/> <module name="Magento_GraphQl"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 383814440df3b..4ee605a01fcd4 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -4,7 +4,7 @@ type Query { storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "Return details about the store's configuration.") @cache(cacheIdentity: "Magento\\StoreGraphQl\\Model\\Resolver\\Store\\ConfigIdentity") availableStores( useCurrentGroup: Boolean @doc(description: "Filter store views by the current store group.") - ): [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") + ): [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") @cache(cacheIdentity: "Magento\\StoreGraphQl\\Model\\Resolver\\Stores\\ConfigIdentity") } type Website @doc(description: "Deprecated. It should not be used on the storefront. Contains information about a website.") { diff --git a/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml b/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml index b63efe9a4dbd5..c5cb15bddfcfb 100644 --- a/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml +++ b/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml @@ -15,6 +15,9 @@ <severity value="CRITICAL"/> <group value="pr_exclude"/> <group value="developer_mode_only"/> + <skip> + <issueId value="ACQE-4803">To be converted to WebApi test</issueId> + </skip> </annotations> <before> <getOTP stepKey="getOtpCode"/> diff --git a/app/code/Magento/SwaggerWebapi/README.md b/app/code/Magento/SwaggerWebapi/README.md index 3529848949d77..7efa4089a4f09 100644 --- a/app/code/Magento/SwaggerWebapi/README.md +++ b/app/code/Magento/SwaggerWebapi/README.md @@ -1 +1 @@ -The Magento_SwaggerWebapi module provides the implementation of the REST Webapi module with Magento_Swagger. \ No newline at end of file +The Magento_SwaggerWebapi module provides the implementation of the REST Webapi module with Magento_Swagger. diff --git a/app/code/Magento/SwaggerWebapiAsync/README.md b/app/code/Magento/SwaggerWebapiAsync/README.md index 373733639c65c..3eeb7a1566c93 100644 --- a/app/code/Magento/SwaggerWebapiAsync/README.md +++ b/app/code/Magento/SwaggerWebapiAsync/README.md @@ -1 +1 @@ -The Magento_SwaggerWebapiAsync module provides the implementation of the Asynchronous WebApi module with Magento_Swagger. \ No newline at end of file +The Magento_SwaggerWebapiAsync module provides the implementation of the Asynchronous WebApi module with Magento_Swagger. diff --git a/app/code/Magento/Swatches/README.md b/app/code/Magento/Swatches/README.md index 801a8f32f3fc5..507ce9a8a02b8 100644 --- a/app/code/Magento/Swatches/README.md +++ b/app/code/Magento/Swatches/README.md @@ -1 +1 @@ -Magento_Swatches module is replacing default product attributes text values with swatch images, for more convenient product displaying and selection. \ No newline at end of file +Magento_Swatches module is replacing default product attributes text values with swatch images, for more convenient product displaying and selection. diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml new file mode 100644 index 0000000000000..97d905e0b3b44 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddTextSwatchToProductWithTwoOptionsActionGroup"> + <annotations> + <description>Add text swatch property attribute.</description> + </annotations> + <arguments> + <argument name="attributeName" defaultValue="{{textSwatchAttribute.default_label}}" type="string"/> + <argument name="attributeCode" defaultValue="{{textSwatchAttribute.attribute_code}}" type="string"/> + <argument name="option1" defaultValue="textSwatchOption1" type="string"/> + <argument name="option2" defaultValue="textSwatchOption2" type="string"/> + <argument name="usedInProductListing" defaultValue="No" type="string"/> + </arguments> + <!--Begin creating text swatch attribute--> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attributeName}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{textSwatchAttribute.input_type}}" stepKey="selectInputType"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('0')}}" userInput="{{option1}}" stepKey="fillSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('0')}}" userInput="{{option1}}" stepKey="fillSwatch1Description"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch2"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('1')}}" userInput="{{option2}}" stepKey="fillSwatch2"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('1')}}" userInput="{{option2}}" stepKey="fillSwatch2Description"/> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attributeCode}}" stepKey="fillAttributeCodeField"/> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="{{usedInProductListing}}" stepKey="useInProductListing"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml new file mode 100644 index 0000000000000..3533d8c343965 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AddVisualSwatchActionGroup"> + <annotations> + <description>Add visual image swatch property attribute.</description> + </annotations> + + <!-- Begin creating a new product attribute of type "Image Swatch" --> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForAttributePageLoad"/> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> + <!-- Select visual swatch --> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="swatch_visual" stepKey="selectInputType"/> + <!-- This hack is because the same <input type="file"> is re-purposed used for all uploads. --> + <executeJS function="HTMLInputElement.prototype.click = function() { if(this.type !== 'file') HTMLElement.prototype.click.call(this); };" stepKey="disableClick"/> + <!-- Set swatch image #1 --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch1"/> + <executeJS function="jQuery('#swatch_window_option_option_0').click()" stepKey="clickSwatch1"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForElementNotVisible selector="{{AdminManageSwatchSection.swatchWindowUnavailable('0')}}" stepKey="waitForImageUploaded1"/> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="visualSwatchOption1" stepKey="fillAdmin1"/> + <fillField selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('0')}}" userInput="visualSwatchOption1" stepKey="fillSwatchDefaultStoreViewBox1"/> + <click selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('0')}}" stepKey="clickOutsideToDisableDropDown"/> + <!-- Set swatch image #2 --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch2"/> + <executeJS function="jQuery('#swatch_window_option_option_1').click()" stepKey="clickSwatch2"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile2"/> + <attachFile selector="input[name='datafile']" userInput="adobe-small.jpg" stepKey="attachFile2"/> + <waitForElementNotVisible selector="{{AdminManageSwatchSection.swatchWindowUnavailable('1')}}" stepKey="waitForImageUploaded2"/> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="visualSwatchOption2" stepKey="fillAdmin2"/> + <fillField selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('1')}}" userInput="visualSwatchOption2" stepKey="fillSwatchDefaultStoreViewBox2"/> + <click selector="{{AdminManageSwatchSection.swatchWindow('1')}}" stepKey="clicksWatchWindow2"/> + <!-- Set scope --> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="Yes" stepKey="useInProductListing"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTop2"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveAndEdit"/> + <wait stepKey="waitToLoad" time="3"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml new file mode 100644 index 0000000000000..b35b1d68b1b12 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateConfigurableProductWithTextSwatchAttributeActionGroup"> + <annotations> + <description>Goes to the Admin Product grid page. Creates a Configurable Product using the default Product Options.</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + <!-- fill in basic configurable product values --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <!-- create configurations for colors the product is available in --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillDefaultLabel"/> + <!-- Change to text swatches --> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="swatch_text" stepKey="selectTextSwatch"/> + <click selector="{{AdminNewAttributePanel.addTextSwatchOption}}" stepKey="clickAddSwatch"/> + <fillField selector="input[name='optiontext[value][option_0][0]']" userInput="Test Text Swatch" stepKey="fillTextSwatchLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml index ce50dd0132101..1c45331b83f8f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml @@ -31,5 +31,12 @@ <element name="nthDelete" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) button.delete-option" parameterized="true"/> <element name="deleteBtn" type="button" selector="#manage-options-panel:nth-of-type({{var}}) button.delete-option" parameterized="true"/> <element name="manageSwatchSection" type="block" selector='//legend/span[contains(text(),"Manage Swatch (Values of Your Attribute)")]'/> + <element name="updateSwatchText" type="input" selector="//td[contains(@class,'col-swatch col-swatch-min-width')][{{index}}]//input" parameterized="true"/> + <element name="updateDescriptionSwatchText" type="input" selector="//td[contains(@class,'col-swatch-min-width swatch-col')][{{index}}]//input[@placeholder='Description']" parameterized="true"/> + <element name="swatchWindowEdit" type="button" selector="//div[@class='swatch_window'][{{args}}]/.." parameterized="true"/> + <element name="updateSwatchTextValues" type="input" selector="//tbody[@data-role='swatch-visual-options-container']//tr[{{row}}]//td[{{col}}]//input" parameterized="true"/> + <element name="nthSwatchWindowEdit" type="button" selector="//tbody[@data-role='swatch-visual-options-container']//tr[{{row}}]//div[@class='swatch_window'][{{col}}]/.." parameterized="true"/> + <element name="defaultLabelField" type="input" selector="//input[@id='attribute_label']"/> + <element name="visualSwatchDefaultStoreViewBox" type="input" selector="input[name='optionvisual[value][option_{{index}}][1]']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml index 43746fc08a0da..4bec27a8d4adc 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -11,5 +11,11 @@ <element name="layeredFilterBlock" type="block" selector="#layered-filter-block"/> <element name="filterOptionTitle" type="button" selector="//div[@class='filter-options-title'][text() = '{{var}}']" parameterized="true" timeout="30"/> <element name="attributeNthOption" type="button" selector="div.{{attributeLabel}} a:nth-of-type({{n}}) div" parameterized="true" timeout="30"/> + <element name="expandedSwatchThumbnails" type="block" selector="//div[@aria-expanded='true' and contains(text(),'{{attribute_code}}')]/..//div[contains(@class,'{{swatch_types}}')]" parameterized="true"/> + <element name="swatchThumbnailsImgLayeredNav" type="block" selector="//div[@class='image' and contains(@style,'{{swatch_thumb}}')]" parameterized="true"/> + <element name="swatchTextLayeredNav" type="block" selector="//div[@class='swatch-option text ' and @data-option-label='{{args}}']" parameterized="true"/> + <element name="swatchTextLayeredNavHover" type="block" selector="//div[@class='title' and text()='{{args}}']" parameterized="true"/> + <element name="swatchSelectedInFilteredProd" type="block" selector="//div[@class='swatch-option {{args}} selected']" parameterized="true"/> + <element name="swatchTextFilteredProdHover" type="block" selector="//div[@class='swatch-option-tooltip']//div[@class='title' and contains(text(),'{{args}}')]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml index 91975e449ff9a..2ce19c8ccf699 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml @@ -16,6 +16,7 @@ check the created attribute is available on the page."/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.xml new file mode 100644 index 0000000000000..cee3b0688f27c --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.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="AdminCreateConfigurableProductWithTextSwatchAttributeTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create congiguration product with text swatches"/> + <title value="Admin can Create congiguration product with text swatches"/> + <description value="Admin can Create congiguration product with text swatches"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5328"/> + </annotations> + <before> + <!-- create category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Login to Admin Portal --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersConfigurable"/> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="addSkuFilterConfigurable"> + <argument name="filterInputName" value="sku"/> + <argument name="filterValue" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <!--Delete created configurable product--> + <actionGroup ref="AdminGridFilterFillSelectFieldActionGroup" stepKey="addTypeFilterConfigurable"> + <argument name="filterName" value="type_id"/> + <argument name="filterValue" value="Configurable Product"/> + </actionGroup> + <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="applyGridFilterConfigurable"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersVirtual"/> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="addSkuFilterVirtual"> + <argument name="filterInputName" value="sku"/> + <argument name="filterValue" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="applyGridFilterVirtual"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteVirtualProducts"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> + <!-- Delete created product attribute --> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteProductAttribute"> + <argument name="productAttributeLabel" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <!-- Reindex after deleting product attribute --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Create a configurable product --> + <actionGroup ref="CreateConfigurableProductWithTextSwatchAttributeActionGroup" stepKey="createConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <!-- click on Next button --> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <!-- Select the created attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(colorProductAttribute.default_label)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <!-- Add the quantities to each SKU's --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml index 14dab5dbb2c85..936e15eacc3e1 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml @@ -15,6 +15,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-4140"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml index 150c0cf13d019..440d6d19d43a5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13641"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml index 07ce30b702f91..639919873649f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-15523"/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml new file mode 100644 index 0000000000000..f5ada80de18a5 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductwithTextandVisualSwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create simple product and configure visual and text swatches"/> + <title value="Admin can create simple product with text and visual swatches"/> + <description value="Admin can create simple product with text and visual swatches"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-5727"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create text and visual swatch attribute--> + <actionGroup ref="AddTextSwatchToProductWithTwoOptionsActionGroup" stepKey="createTextSwatch"/> + <actionGroup ref="AddVisualSwatchActionGroup" stepKey="createVisualSwatch"/> + <!--Assign text swatch attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Assign visual swatch attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage1"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet1"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup1"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet1"/> + <!--Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create product and fill new text swatch attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Add text swatch product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + <click selector="{{AdminProductFormSection.saveCategory}}" stepKey="saveCategory"/> + <scrollToTopOfPage stepKey="scrollToTop0"/> + <selectOption selector="{{AdminProductFormSection.attributeRequiredInputField(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="fillTheAttributeRequiredInputField"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Create product and fill new visual swatch attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad stepKey="waitForProductIndexPage1"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct1"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm1"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <!-- Add visual swatch product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory1"/> + <click selector="{{AdminProductFormSection.saveCategory}}" stepKey="saveCategory1"/> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <selectOption selector="{{AdminProductFormSection.attributeRequiredInputField(ProductAttributeFrontendLabel.label)}}" userInput="visualSwatchOption2" stepKey="fillTheAttributeRequiredInputField1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex1"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <!-- Delete text and visual swatch attributes --> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid0"> + <argument name="productAttributeCode" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode0"> + <argument name="productAttributeCode" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess0"/> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid1"> + <argument name="productAttributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode1"> + <argument name="productAttributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete product --> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Assert that attribute values present in layered navigation --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <click selector="{{StorefrontCategorySidebarSection.seeLayeredNavigationCategoryTextSwatch}}" stepKey="clickTextSwatch"/> + <click selector="{{StorefrontCategorySidebarSection.seeTextSwatchOption}}" stepKey="seeTextSwatch"/> + <see userInput="{{SimpleProduct.name}}" stepKey="assertTextSwatchProduct"/> + <!--Assert that attribute values present in layered navigation --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <click selector="{{StorefrontCategorySidebarSection.seeLayeredNavigationCategoryVisualSwatch}}" stepKey="clickVisualSwatch"/> + <click selector="{{StorefrontCategorySidebarSection.seeVisualSwatchOption}}" stepKey="seeVisualSwatch"/> + <see userInput="{{DownloadableProduct.name}}" stepKey="assertVisualSwatchProduct"/> + <!--Verfiy the text swatch attribute product appears in search option with option one --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionOne"> + <argument name="phrase" value="textSwatchOption1"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeTextSwatchAttributeProductName"/> + <!--Verfiy the text swatch attribute product does not appears in search option with option two --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage1"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionTwo"> + <argument name="phrase" value="textSwatchOption2"/> + </actionGroup> + <dontSee selector="{{StorefrontCatalogSearchMainSection.searchResults}}" userInput="{{SimpleProduct.name}}" stepKey="doNotSeeProduct"/> + <!--Verfiy the visual swatch attribute product appears in search option with option two --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage2"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionTwo1"> + <argument name="phrase" value="visualSwatchOption2"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{DownloadableProduct.name}}" stepKey="seeVisualSwatchAttributeProductName"/> + <!--Verfiy the visual swatch attribute product does not appears in search option with option one --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage3"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionOne1"> + <argument name="phrase" value="visualSwatchOption1"/> + </actionGroup> + <dontSee selector="{{StorefrontCatalogSearchMainSection.searchResults}}" userInput="{{DownloadableProduct.name}}" stepKey="doNotSeeProduct1"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..570c75d18fa6e --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product attributes"/> + <title value="Create Configurable product based on Visual Swatch attribute with Images and custom labels on different Store Views"/> + <description value="Create Configurable product based on Visual Swatch attribute with Images and custom labels on different Store Views"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-5691"/> + <group value="product"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create a second Store View --> + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + </before> + <after> + <!-- Delete all created product --> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProducts"> + <argument name="sku" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete product attribute and clear grid filter --> + <deleteData createDataKey="createVisualSwatchAttribute" stepKey="deleteVisualSwatchAttribute"/> + <!-- Delete Store view --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Step1: Create Visual Swatch attribute --> + <createData entity="VisualSwatchProductAttributeForm" stepKey="createVisualSwatchAttribute"/> + <magentoCLI stepKey="reindexPostCreatingVisualSwatchAttribute" command="indexer:reindex"/> + <magentoCLI stepKey="flushCachePostCreatingVisualSwatchAttribute" command="cache:flush"/> + <!-- Go to the edit page for the visual Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditVisualSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createVisualSwatchAttribute.attribute_code$" stepKey="fillFilterToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createVisualSwatchAttribute.attribute_code$')}}" stepKey="clickVisualSwatchRowToEdit"/> + <grabValueFrom selector="{{AdminManageSwatchSection.defaultLabelField}}" stepKey="grabAttributeValue"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption1"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('1','1')}}" stepKey="clickSwatchButtonToEditForOption1"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1ForOption1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption1"/> + <waitForPageLoad stepKey="waitFileAttachedForOption1"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','4')}}" stepKey="clickOutsideTheDropdownForOption1"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','4')}}" userInput="A1" stepKey="addA1valueToAdmin"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','5')}}" userInput="B1" stepKey="addB1valueToDefault"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','6')}}" userInput="C1" stepKey="addC1valueToSecondStore"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption2"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('2','1')}}" stepKey="clickSwatchButtonToEditForOption2"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile1ForOption2"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption2"/> + <waitForPageLoad stepKey="waitFileAttachedForOption2"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','4')}}" stepKey="clickOutsideTheDropdownForOption2"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','4')}}" userInput="A2" stepKey="addA2valueToAdmin"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','5')}}" userInput="B2" stepKey="addB2valueToDefault"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption3"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('3','1')}}" stepKey="clickSwatchButtonToEditForOption3"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('3')}}" stepKey="clickUploadFile1ForOption3"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption3"/> + <waitForPageLoad stepKey="waitFileAttachedForOption3"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('3','4')}}" stepKey="clickOutsideTheDropdownForOption3"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('3','4')}}" userInput="A3" stepKey="addA3valueToAdmin"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption4"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('4','4')}}" userInput="A4" stepKey="addA4valueToAdmin"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForVisualSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForVisualSwatchAttribute"/> + <!-- Add created product attribute to the Default set --> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createVisualSwatchAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!-- Step2: Create configurable product --> + <createData entity="_defaultCategory" stepKey="createCategory" /> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <!-- Click "Create Configurations" button, select created product attribute using the same Quantity for all products. Click "Generate products" button --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd1"/> + <click selector="{{AdminGridRow.checkboxByValue('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd1"/> + <!-- Step3: Navigate to default store view --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToStorefrontCategoryPageForDefaultStoreLayeredNavigation"> + <argument name="categoryName" value="$$createCategory.name$$" /> + </actionGroup> + <!-- Step4 5 and 8: Verify the attributes in Layered Navigation and Product details for Default Store view --> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('{$grabAttributeValue}')}}" stepKey="expandVisualSwatchAttributeInLayeredNavForDefaultStoreView"/> + <waitForElementVisible selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B1')}}" stepKey="waitForSwatchSystemValueVisibleForDefaultStore"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInLayeredNav"/> + <!-- Verify the attributes in Product Details --> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInListedProduct"/> + <!-- Step4 5 and 8: Verify the attributes in Layered Navigation and Product details for Secondary Store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchToSecondStoreViewForLayeredNavigation"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickOnCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoadForSecondaryStore"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('{$grabAttributeValue}')}}" stepKey="expandVisualSwatchAttributeInLayeredNavForSecondaryStoreView"/> + <waitForElementVisible selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('C1')}}" stepKey="waitForSwatchSystemValueVisibleForSecondaryView"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInLayeredNav"/> + <!-- Step8: Verify the attributes in Products page in Secondary Store view --> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInListedProduct"/> + <!-- Verify the product present in product page of the storefront defult view --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnDefaultStorefront"/> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductPage"/> + <!-- Verify the attributes in Product Details page for Default Store --> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <!-- Verify the product present in product page of the storefront secondary view --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnSecondaryStorefront"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Verify the attributes in Product Details page for Secondary Store View --> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <!-- Verify the attributes for Product Search page for Default Store View --> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductSearchPage"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchProductOnStorefrontForDefaultStoreView"> + <argument name="phrase" value="$$createConfigurableProduct.name$$"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductSearchInDefaultStore"/> + <!-- Verify the attributes for Product Search page for Secondary Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductSearchPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductSearchInSecondaryStore"/> + <!-- Verify the attributes for Product in cart for Default Store View --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnDefaultStorefrontForShoppingCart"/> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductPageToAddToCart"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B1')}}" stepKey="clickB1SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addB1productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForB1addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B2')}}" stepKey="clickB2SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addB2productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForB2addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="clickA3SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addA3productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForA3addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="clickA4SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addA4productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForA4addedToCartFromDefaultStoreView"/> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMiniCartForDefaultStore"/> + <waitForPageLoad stepKey="waitForViewAndEditCartToOpenForDefaultStore"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearForDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductInCartInDefaultStore"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductInCartPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductInCartInSecondaryStore"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml index e5f9b70f1af69..14847361f9ebe 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-19683"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <!--Go to category page--> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml new file mode 100644 index 0000000000000..eb08b639dce89 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest"> + <annotations> + <features value="Swatches"/> + <stories value="Configurable product with two swatch attributes and display out of stock enabled"/> + <title value="Configurable product with two swatch attributes and display out of stock enabled"/> + <description value="Storefront selection of out of stock child products of configurable products are + disabled when display out of stock options are enabled"/> + <severity value="MAJOR"/> + <testCaseId value="AC-7020"/> + <useCaseId value="ACP2E-1342"/> + <group value="Swatches"/> + <group value="cloud"/> + </annotations> + <before> + <!--Set Display out of stock Enabled--> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="setDisplayOutOfStockProduct"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable Product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Set Display out of stock Disabled--> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="setDisplayOutOfStockProduct"/> + <!--Delete Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete Configurable Product--> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProducts"> + <argument name="sku" value="{{ApiConfigurableProduct.sku}}"/> + </actionGroup> + <!--Clear Filters--> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="clearFilters"/> + <!--Delete Color Attribute--> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteColorAttribute"> + <argument name="ProductAttribute" value="ProductColorAttribute"/> + </actionGroup> + <!--Delete Size Attribute--> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteSizeAttribute"> + <argument name="ProductAttribute" value="ProductSizeAttribute"/> + </actionGroup> + <!--Logout from Admin Area--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--Create Color Attribute--> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addColorAttribute"> + <argument name="attributeName" value="{{ProductColorAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="option1" value="Black"/> + <argument name="option2" value="White"/> + <argument name="option3" value="Blue"/> + </actionGroup> + <!--Create Size swatch attribute with 3 options: Small, Medium and Large--> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addSizeAttribute"> + <argument name="attributeName" value="{{ProductSizeAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductSizeAttribute.attribute_code}}"/> + <argument name="option1" value="Small"/> + <argument name="option2" value="Medium"/> + <argument name="option3" value="Large"/> + </actionGroup> + <!--Go to product page and Configure Size and Color--> + <amOnPage url="{{AdminProductEditPage.url($createConfigurableProduct.id$)}}" stepKey="goToConfigurableProduct"/> + <actionGroup ref="CreateConfigurationsForTwoAttributeActionGroup" stepKey="createConfigurations"> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="secondAttributeCode" value="{{ProductSizeAttribute.attribute_code}}"/> + </actionGroup> + <!--Make Simple product OOS--> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$-Blue-Medium"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="outOfStockStatus"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> + <!--Perform Reindex--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!--Go to Storefront Product Page--> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> + <argument name="productUrl" value="$createConfigurableProduct.custom_attributes[url_key]$"/> + </actionGroup> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Blue')}}" + stepKey="clickBlueAttribute"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Medium')}}" + userInput="disabled" stepKey="grabMediumAttribute"/> + <assertEquals stepKey="assertMediumDisabled"> + <actualResult type="const">$grabMediumAttribute</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Large')}}" + stepKey="clickLargeAttribute"/> + <see selector="{{StorefrontProductInfoMainSection.selectedSwatchValue('Large')}}" userInput="Large" stepKey="seeSwatchSizeLargeBecomeSelected"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml index 02d08f52d9017..8f8ad9fe1b6ab 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml @@ -19,6 +19,7 @@ (visible and active) for each selected option for the configurable product"/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Go to created attribute (attribute page) --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index 8ecae7e0137a1..501f1c3ea677d 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3461"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 0d28be1b94638..f32eb128544f5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3462"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index 262d9fd7c4c4a..c58ae1b0fc0cd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3082"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml index 3288abbbb8d27..995b933e43d9a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-18821"/> <testCaseId value="MC-11531"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <!--Create category and configurable product with two options--> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml index 0add6159d5e40..9ceedfb0392aa 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml @@ -21,6 +21,7 @@ to selected needed option."/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Go to created attribute (attribute page) --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..93563c1c32b18 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SwatchesAreVisibleInLayeredNavigationTest"> + <annotations> + <features value="Swatches"/> + <stories value="Swatches are visible in Layered Navigation"/> + <title value="Swatches are visible in Layered Navigation"/> + <description value="Swatches are visible in Layered Navigation"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4154"/> + <group value="Swatches"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigurableProduct1" stepKey="deleteConfigurableProduct1"/> + <deleteData createDataKey="createConfigurableProduct2" stepKey="deleteConfigurableProduct2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createTextSwatchAttribute" stepKey="deleteTextSwatchAttribute"/> + <deleteData createDataKey="createVisualSwatchAttribute" stepKey="deleteVisualSwatchAttribute"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!-- Create 2 Configurable products --> + <createData entity="_defaultCategory" stepKey="createCategory" /> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create product visual swatch attribute --> + <createData entity="VisualSwatchProductAttributeForm" stepKey="createVisualSwatchAttribute"/> + <createData entity="SwatchProductAttributeOption1" stepKey="visualSwatchAttributeOption"> + <requiredEntity createDataKey="createVisualSwatchAttribute"/> + </createData> + <!-- Create product text swatch attribute --> + <createData entity="TextSwatchProductAttributeForm" stepKey="createTextSwatchAttribute"/> + <createData entity="SwatchProductAttributeOption1" stepKey="textSwatchAttributeOption"> + <requiredEntity createDataKey="createTextSwatchAttribute"/> + </createData> + <magentoCLI stepKey="reindexPostCreating2Attributes" command="indexer:reindex"/> + <magentoCLI stepKey="flushCachePostCreating2Attributes" command="cache:flush"/> + <!-- Go to the edit page for the visual Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditVisualSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createVisualSwatchAttribute.attribute_code$" stepKey="fillFilterToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createVisualSwatchAttribute.attribute_code$')}}" stepKey="clickVisualSwatchRowToEdit"/> + <click selector="{{AdminManageSwatchSection.swatchWindowEdit('1')}}" stepKey="clickSwatchButtonToEdit"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForPageLoad stepKey="waitFileAttached1"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForVisualSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForVisualSwatchAttribute"/> + <!-- Go to the edit page for the text Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditTextSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createTextSwatchAttribute.attribute_code$" stepKey="fillFilterToEditTextSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditTextSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createTextSwatchAttribute.attribute_code$')}}" stepKey="clickTextSwatchRowToEdit"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchText('1')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionAdminName"/> + <fillField selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('1')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDescription"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchText('2')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDefaultStoreViewName"/> + <fillField selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('2')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDefaultStoreViewDescription"/> + <grabValueFrom selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('2')}}" stepKey="grabTextValue"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForTextSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForTextSwatchAttribute"/> + <!-- Update Config product1 visual swatch attribute --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct1$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct1$$"/> + </actionGroup> + <!-- Edit the configurable product 1 --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd1"/> + <click selector="{{AdminGridRow.checkboxByValue('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd1"/> + <!-- Update Config product2 visual swatch attribute --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct2"> + <argument name="product" value="$$createConfigurableProduct2$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct2"> + <argument name="product" value="$$createConfigurableProduct2$$"/> + </actionGroup> + <!-- Edit the configurable product 2 --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd2"/> + <click selector="{{AdminGridRow.checkboxByValue('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd2"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd2"/> + <!-- Go to the Storefront category page --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <!-- Verify swatches are present in the layered navigation --> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="$createVisualSwatchAttribute.frontend_label[0]$" stepKey="seeVisualSwatchAttributeInLayeredNav"/> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="$createTextSwatchAttribute.frontend_label[0]$" stepKey="seeTextSwatchAttributeInLayeredNav"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="expandVisualSwatchAttribute"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createVisualSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="hoverOverSwatchAttribute"/> + <waitForPageLoad stepKey="waitForHoveredImageToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchThumbnailsImgLayeredNav('swatch_thumb')}}" stepKey="seeSwatchImageOnHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav1"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="expandTextSwatchAttribute"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchTextLayeredNav('${grabTextValue}')}}" stepKey="hoverOverTextAttribute"/> + <waitForPageLoad stepKey="waitForHoveredTextToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchTextLayeredNavHover('${grabTextValue}')}}" stepKey="seeSwatchTextOnHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav2"/> + <!-- Verify the swatches on displayed product --> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="expandVisualSwatchAttributeToClick"/> + <click selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createVisualSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="clickOverSwatchAttribute"/> + <waitForPageLoad stepKey="waitForSwatchImageFilteredProductToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('image')}}" stepKey="seeSwatchImageOnFilteredProduct"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('image')}}" stepKey="hoverOverSwatchImageOnFilteredProduct"/> + <waitForPageLoad stepKey="waitForHoveredImageToLoadForFilteredProduct"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchThumbnailsImgLayeredNav('swatch_thumb')}}" stepKey="seeSwatchImageOnFilteredProductHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav3"/> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeFilter"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="expandTextSwatchAttributeToClick"/> + <click selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createTextSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="clickOverTextAttribute"/> + <waitForPageLoad stepKey="waitForSwatchTextFilteredProductToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('text')}}" stepKey="seeSwatchTextOnFilteredProduct"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('text')}}" stepKey="hoverOverSwatchTextOnFilteredProduct"/> + <waitForPageLoad stepKey="waitForHoveredTextToLoadForFilteredProduct"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchTextFilteredProdHover('${grabTextValue}')}}" stepKey="seeSwatchTextOnFilteredProductHover"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php index 9e7e62e0a077f..7f58641d4d227 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php @@ -23,8 +23,11 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Pricing\PriceInfo\Base; use Magento\Framework\Stdlib\ArrayUtils; @@ -95,8 +98,23 @@ class ConfigurableTest extends TestCase */ private $request; + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + protected function setUp(): void { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMockForAbstractClass(); + \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); $this->arrayUtils = $this->createMock(ArrayUtils::class); $this->jsonEncoder = $this->getMockForAbstractClass(EncoderInterface::class); $this->helper = $this->createMock(Data::class); @@ -127,6 +145,16 @@ protected function setUp(): void $context = $this->getContextMock(); $context->method('getRequest')->willReturn($this->request); + $this->deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); + + $this->deploymentConfig->expects($this->any()) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY) + ->willReturn('448198e08af35844a42d3c93c1ef4e03'); + $objectManagerHelper = new ObjectManager($this); $this->configurable = $objectManagerHelper->getObject( ConfigurableRenderer::class, @@ -146,7 +174,7 @@ protected function setUp(): void 'configurableAttributeData' => $this->configurableAttributeData, 'data' => [], 'variationPrices' => $this->variationPricesMock, - 'customerSession' => $customerSession, + 'customerSession' => $customerSession ] ); } @@ -308,6 +336,10 @@ public function testGetCacheKey() ->willReturn($configurableAttributes); $this->request->method('toArray')->willReturn($requestParams); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(DeploymentConfig::class) + ->willReturn($this->deploymentConfig); $this->assertStringContainsString( sha1(json_encode(['color' => 59, 'size' => 1])), $this->configurable->getCacheKey() diff --git a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js index 740eb5e07b99b..734cde2b4cc36 100644 --- a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js @@ -465,12 +465,17 @@ define([ // Aggregate options array to hash (key => value) $.each(item.options, function () { if (this.products.length > 0) { + let salableProducts = this.products; + + if ($widget.options.jsonConfig.canDisplayShowOutOfStockStatus) { + salableProducts = $widget.options.jsonConfig.salable[item.id][this.id]; + } $widget.optionsMap[item.id][this.id] = { price: parseInt( $widget.options.jsonConfig.optionPrices[this.products[0]].finalPrice.amount, 10 ), - products: this.products + products: salableProducts }; } }); diff --git a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls index e3157b934b6ae..b4ec69801ab7d 100644 --- a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls @@ -51,3 +51,30 @@ type ColorSwatchData implements SwatchDataInterface { type ConfigurableProductOptionValue { swatch: SwatchDataInterface @resolver(class: "Magento\\SwatchesGraphQl\\Model\\Resolver\\Product\\Options\\SwatchData") @doc(description: "The URL assigned to the thumbnail of the swatch image.") } + +type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Swatch attribute metadata.") { + swatch_input_type: SwatchInputTypeEnum @doc(description: "Input type of the swatch attribute option.") + update_product_preview_image: Boolean @doc(description: "Whether update product preview image or not.") + use_product_image_for_swatch: Boolean @doc(description: "Whether use product image for swatch or not.") +} + +enum SwatchInputTypeEnum @doc(description: "Swatch attribute metadata input types.") { + BOOLEAN + DATE + DATETIME + DROPDOWN + FILE + GALLERY + HIDDEN + IMAGE + MEDIA_IMAGE + MULTILINE + MULTISELECT + PRICE + SELECT + TEXT + TEXTAREA + UNDEFINED + VISUAL + WEIGHT +} diff --git a/app/code/Magento/SwatchesLayeredNavigation/README.md b/app/code/Magento/SwatchesLayeredNavigation/README.md index 7199bfa628634..fb21b4dc10de4 100644 --- a/app/code/Magento/SwatchesLayeredNavigation/README.md +++ b/app/code/Magento/SwatchesLayeredNavigation/README.md @@ -5,16 +5,20 @@ The **Magento_SwatchesLayeredNavigation** module enables LayeredNavigation functionality for Swatch attributes ## Backward incompatible changes + No backward incompatible changes ## Dependencies + The **Magento_SwatchesLayeredNavigation** is dependent on the following modules: - Magento_Swatches - Magento_LayeredNavigation ## Specific Settings + The **Magento_SwatchesLayeredNavigation** module does not provide any specific settings. ## Specific Extension Points + The **Magento_SwatchesLayeredNavigation** module does not provide any specific extension points. You can extend it using the Magento extension mechanism. diff --git a/app/code/Magento/Tax/Model/Calculation.php b/app/code/Magento/Tax/Model/Calculation.php index 030b2974ce978..6ccf10e6d181b 100644 --- a/app/code/Magento/Tax/Model/Calculation.php +++ b/app/code/Magento/Tax/Model/Calculation.php @@ -15,51 +15,53 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Store\Model\Store; use Magento\Tax\Api\TaxClassRepositoryInterface; /** * Tax Calculation Model + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Calculation extends \Magento\Framework\Model\AbstractModel +class Calculation extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * Identifier constant for Tax calculation before discount excluding TAX */ - const CALC_TAX_BEFORE_DISCOUNT_ON_EXCL = '0_0'; + public const CALC_TAX_BEFORE_DISCOUNT_ON_EXCL = '0_0'; /** * Identifier constant for Tax calculation before discount including TAX */ - const CALC_TAX_BEFORE_DISCOUNT_ON_INCL = '0_1'; + public const CALC_TAX_BEFORE_DISCOUNT_ON_INCL = '0_1'; /** * Identifier constant for Tax calculation after discount excluding TAX */ - const CALC_TAX_AFTER_DISCOUNT_ON_EXCL = '1_0'; + public const CALC_TAX_AFTER_DISCOUNT_ON_EXCL = '1_0'; /** * Identifier constant for Tax calculation after discount including TAX */ - const CALC_TAX_AFTER_DISCOUNT_ON_INCL = '1_1'; + public const CALC_TAX_AFTER_DISCOUNT_ON_INCL = '1_1'; /** * Identifier constant for unit based calculation */ - const CALC_UNIT_BASE = 'UNIT_BASE_CALCULATION'; + public const CALC_UNIT_BASE = 'UNIT_BASE_CALCULATION'; /** * Identifier constant for row based calculation */ - const CALC_ROW_BASE = 'ROW_BASE_CALCULATION'; + public const CALC_ROW_BASE = 'ROW_BASE_CALCULATION'; /** * Identifier constant for total based calculation */ - const CALC_TOTAL_BASE = 'TOTAL_BASE_CALCULATION'; + public const CALC_TOTAL_BASE = 'TOTAL_BASE_CALCULATION'; /** * Identifier constant for unit based calculation @@ -168,22 +170,16 @@ class Calculation extends \Magento\Framework\Model\AbstractModel protected $priceCurrency; /** - * Filter Builder - * * @var FilterBuilder */ protected $filterBuilder; /** - * Search Criteria Builder - * * @var SearchCriteriaBuilder */ protected $searchCriteriaBuilder; /** - * Tax Class Repository - * * @var TaxClassRepositoryInterface */ protected $taxClassRepository; @@ -249,6 +245,8 @@ public function __construct( } /** + * Tax Calculation Model Contructor + * * @return void */ protected function _construct() @@ -494,7 +492,7 @@ protected function _isCrossBorderTradeEnabled($store = null) * @param null|int $customerTaxClass * @param null|int|\Magento\Store\Model\Store $store * @param int $customerId - * @return \Magento\Framework\DataObject + * @return \Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -529,11 +527,13 @@ public function getRateRequest( //fallback to default address for registered customer try { $defaultBilling = $this->customerAccountManagement->getDefaultBillingAddress($customerId); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } try { $defaultShipping = $this->customerAccountManagement->getDefaultShippingAddress($customerId); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } @@ -650,6 +650,7 @@ public function reproduceProcess($rates) /** * Calculate rated tax amount based on price and tax rate. + * * If you are using price including tax $priceIncludeTax should be true. * * @param float $price @@ -687,6 +688,8 @@ public function round($price) } /** + * Get Tax Rates + * * @param array $billingAddress * @param array $shippingAddress * @param int $customerTaxClassId @@ -720,4 +723,16 @@ public function getTaxRates($billingAddress, $shippingAddress, $customerTaxClass } return $productRates; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_rates = []; + $this->_ctc = []; + $this->_ptc = []; + $this->_rateCache = []; + $this->_rateCalculationProcess = []; + } } diff --git a/app/code/Magento/Tax/Model/ClassModelRegistry.php b/app/code/Magento/Tax/Model/ClassModelRegistry.php index 668d104f3ccfb..a3fa80db83f1b 100644 --- a/app/code/Magento/Tax/Model/ClassModelRegistry.php +++ b/app/code/Magento/Tax/Model/ClassModelRegistry.php @@ -7,17 +7,16 @@ namespace Magento\Tax\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Tax\Model\ClassModel as TaxClassModel; use Magento\Tax\Model\ClassModelFactory as TaxClassModelFactory; /** * Registry for the tax class models */ -class ClassModelRegistry +class ClassModelRegistry implements ResetAfterRequestInterface { /** - * Tax class model factory - * * @var TaxClassModelFactory */ private $taxClassModelFactory; @@ -82,4 +81,12 @@ public function remove($taxClassId) { unset($this->taxClassRegistryById[$taxClassId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->taxClassRegistryById = []; + } } diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php index 646da1441f225..902f7c2fc68c9 100644 --- a/app/code/Magento/Tax/Model/Config.php +++ b/app/code/Magento/Tax/Model/Config.php @@ -11,6 +11,7 @@ */ namespace Magento\Tax\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -18,7 +19,7 @@ * * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ -class Config +class Config implements ResetAfterRequestInterface { /** * Tax notifications @@ -952,4 +953,14 @@ public function needPriceConversion($store = null) } return $res; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_priceIncludesTax = null; + $this->_shippingPriceIncludeTax = null; + $this->_needUseShippingExcludeTax = false; + } } diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation.php index 00de17ff5d3bf..036d8533a585e 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation.php @@ -9,7 +9,9 @@ */ namespace Magento\Tax\Model\ResourceModel; -class Calculation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + +class Calculation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** * Store ISO 3166-1 alpha-2 USA country code @@ -473,4 +475,12 @@ public function getRateIds($request) return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_ratesCache = []; + } } diff --git a/app/code/Magento/Tax/Model/TaxCalculation.php b/app/code/Magento/Tax/Model/TaxCalculation.php index ac18dfec6c7ed..de0289529980b 100644 --- a/app/code/Magento/Tax/Model/TaxCalculation.php +++ b/app/code/Magento/Tax/Model/TaxCalculation.php @@ -6,6 +6,7 @@ namespace Magento\Tax\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Tax\Api\TaxCalculationInterface; use Magento\Tax\Api\TaxClassManagementInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; @@ -23,7 +24,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TaxCalculation implements TaxCalculationInterface +class TaxCalculation implements TaxCalculationInterface, ResetAfterRequestInterface { /** * Tax Details factory @@ -80,15 +81,11 @@ class TaxCalculation implements TaxCalculationInterface private $parentToChildren; /** - * Tax Class Management - * * @var TaxClassManagementInterface */ protected $taxClassManagement; /** - * Calculator Factory - * * @var CalculatorFactory */ protected $calculatorFactory; @@ -129,7 +126,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function calculateTax( \Magento\Tax\Api\Data\QuoteDetailsInterface $quoteDetails, @@ -200,7 +197,7 @@ public function calculateTax( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultCalculatedRate( $productTaxClassID, @@ -211,7 +208,7 @@ public function getDefaultCalculatedRate( } /** - * {@inheritdoc} + * @inheritdoc */ public function getCalculatedRate( $productTaxClassID, @@ -290,6 +287,7 @@ protected function processItem( * @param TaxDetailsItemInterface[] $children * @param int $quantity * @return TaxDetailsItemInterface + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function calculateParent($children, $quantity) { @@ -386,4 +384,13 @@ protected function getTotalQuantity(QuoteDetailsItemInterface $item) } return $item->getQuantity(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->keyedItems = []; + $this->parentToChildren = []; + } } diff --git a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php index bad9757dafd89..2a354ea4376da 100644 --- a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php +++ b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php @@ -8,15 +8,14 @@ use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Modifies the bundle config for the front end to resemble the tax included price when tax included prices. */ -class GetPriceConfigurationObserver implements ObserverInterface +class GetPriceConfigurationObserver implements ObserverInterface, ResetAfterRequestInterface { /** - * Tax data - * * @var \Magento\Tax\Helper\Data */ protected $taxData; @@ -146,4 +145,12 @@ private function updatePriceForBundle($holder, $key) } return $holder; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->selectionCache = []; + } } diff --git a/app/code/Magento/Tax/README.md b/app/code/Magento/Tax/README.md index f449b1c6c6cba..5f3193d438214 100644 --- a/app/code/Magento/Tax/README.md +++ b/app/code/Magento/Tax/README.md @@ -1,11 +1,14 @@ # Overview + ## Purpose of module + The Magento_Tax module provides the calculations needed to compute the consumption tax on goods and services. The Magento_Tax module includes the following: + * configuration of the tax rates and rules to apply * configuration of tax classes that apply to: -** taxation on products +**taxation on products ** taxation on shipping charges ** taxation on gift options (example: gift wrapping) * specification whether the consumption tax is "sales & use" (typically product prices are loaded without any tax) or "VAT" (typically product prices are loaded including tax) @@ -13,20 +16,25 @@ The Magento_Tax module includes the following: * display of prices (presented with tax, without tax, or both with and without) The Magento_Tax module also handles special cases when computing tax, such as: + * determining the tax on an individual item (for example, one that is being returned) when the original tax has been computed on the entire shopping cart ** example country: United States * being able to handle 2 or more tax rates that are applied separately (examples include a "luxury tax" on exclusive items) * being able to handle a subsequent tax rate that is applied after a previous one is applied (a "tax on tax" situation, which recently was a part of Canadian tax law) # Deployment + ## System requirements + The Magento_Tax module does not have any specific system requirements. Depending on how many tax rates and tax rules are being used, there might be consideration for the database size Depending on the frequency of updating tax rates and tax rules, there might be consideration for the scheduling of these updates ## Install + The Magento_Tax module is installed automatically (using the native Magento install mechanism) without any additional actions. ## Uninstall -The Magento_Tax module should not be uninstalled; it is a required module. \ No newline at end of file + +The Magento_Tax module should not be uninstalled; it is a required module. diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml index 97d59a51bb68e..5424d6549ac67 100644 --- a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AddCustomTaxRateActionGroup" extends="addNewTaxRateNoZip"> + <actionGroup name="AddCustomTaxRateActionGroup" extends="AddNewTaxRateNoZipActionGroup"> <annotations> <description>EXTENDS: addNewTaxRateNoZip. Removes 'fillZipCode' and 'fillRate'. Fills in the Zip Code and Rate. PLEASE NOTE: The values are Hardcoded.</description> </annotations> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml index bc6099790431f..4bc1c068570f4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml @@ -147,4 +147,7 @@ <entity name="SecondTaxRateTexas" extends="TaxRateTexas"> <data key="rate">0.125</data> </entity> + <entity name="ThirdTaxRateTexas" extends="TaxRateTexas"> + <data key="rate">20</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml b/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml new file mode 100644 index 0000000000000..1e6bb20ad7a8c --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml @@ -0,0 +1,16 @@ +<?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="TaxSuite"> + <include> + <group name="tax_isolated"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml index 9b67208f77492..6e077525f41ef 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml @@ -62,7 +62,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!--Create new order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="Simple_US_Customer_NY"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index b2fd51225eaa6..c671339c2c2ba 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-25815"/> <useCaseId value="MAGETWO-91521"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and product --> @@ -141,7 +142,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProduct"/> <!--Create an order with these 2 products in that zip code.--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrder"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrder"/> <!--Check if order can be submitted without the required fields including email address--> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductButton"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml index 2f418dddf3884..1bc04926ae2f4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml index 0ede3caacd867..7b8816cbc0b3f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml index cb597273e36b6..39cb03a8a5985 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml index 46d3582681c56..bb2d59d11caba 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml index f428aabddcf9a..1c8ffa4d84976 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml index 0e541b8939053..aa87bfd729cc7 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml index 7b9712fc30a42..3a791336c5616 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml @@ -18,6 +18,7 @@ <group value="tax"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml index 21f8b844adb58..84d1f1b162b09 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml index 25b919722ced9..ca70751dfe069 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml index 7ba6caf5402b1..f198197ecdc67 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml index 5cc17527c9801..8800713d9ae89 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml index a091fa5c9960f..4189c52d9dbe9 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml index 49c686618245d..a6037102a7b2b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml index 8cd85ee0ca969..ce5cd6d571f34 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11026"/> <useCaseId value="MC-4316"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml index addd8d2832417..bf1a72a626e00 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml index 65945f80048ad..0715af73e1bf1 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml index c208912654fdb..2c594399cf7da 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml index 711307b6579cb..32b3e126f085f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml @@ -15,6 +15,7 @@ <description value="Verify Tax is calculated based on Tax Rule even if all Shipping methods are disabled"/> <testCaseId value="AC-3895"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml index ec40cd835d381..9253dc0a01c65 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml @@ -16,6 +16,7 @@ <description value="Apply tax and fpt for simple product with canadian pst origin test"/> <severity value="MAJOR"/> <testCaseId value="AC-4061"/> + <group value="tax_isolated" /> </annotations> <before> <!-- Create a new user with canadian address --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml index 5f288d55b5d05..04493111d0044 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml b/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml index c5749a3a091d0..5a5ae1e153da5 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="AC-3201"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml index 1593f91945b97..7f6ac77aff57d 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml @@ -16,8 +16,10 @@ <severity value="CRITICAL"/> <testCaseId value="MC-296"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="VirtualProduct" stepKey="virtualProduct1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml index 215d2b82a8180..a880e2de64c58 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml @@ -54,15 +54,9 @@ <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> - </actionGroup> - - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> - </actionGroup> + <actionGroup ref="AdminDeleteMultipleTaxRatesActionGroup" stepKey="deleteAllNonDefaultTaxRates"/> + <comment userInput="Preserve BiC" stepKey="deleteNYRate"/> + <comment userInput="Preserve BiC" stepKey="deleteCARate"/> <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> @@ -86,6 +80,7 @@ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> <argument name="Address" value="US_Address_CA"/> </actionGroup> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="checkFlatRateShippingMethod" /> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml index 52acb40a5b023..456ff2e091044 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-255"/> <group value="Tax"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml index 7ced4d382135f..3869a02a18e11 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-256"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml index 6f7ef59788f68..58ef84485e965 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml index c7663acf97a14..227329ceda050 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml index 5776925354e80..1d3923d954dda 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml index c4449e5d6e5ad..e414b04ea6fc6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml index 2bac4ca2115c0..328cb58d5c39c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml index c808de2d7f10d..16ac05506c05c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php index ac13f8a5e8fe8..a48325bee1371 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php @@ -134,7 +134,7 @@ public function testCollectDoesNotCalculateTaxIfThereIsNoItemsRelatedToGivenAddr public function testCollect() { - $this->markTestIncomplete('Target code is not unit testable. Refactoring is required.'); + $this->markTestSkipped('Target code is not unit testable. Refactoring is required.'); } /** diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php index 2d45da37d0107..2fe634b1ff840 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php @@ -48,7 +48,7 @@ */ class TaxTest extends TestCase { - const TAX = 0.2; + public const TAX = 0.2; /** * Tests the specific method @@ -72,7 +72,7 @@ public function testCollect( $addressData, $verifyData ) { - $this->markTestIncomplete('Source code is not testable. Need to be refactored before unit testing'); + $this->markTestSkipped('Source code is not testable. Need to be refactored before unit testing'); $shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); $totalsMock = $this->createMock(Total::class); $objectManager = new ObjectManager($this); diff --git a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml index b83fe02f897a1..e17c2d86d7a5d 100644 --- a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml +++ b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml @@ -24,6 +24,7 @@ <testCaseId value="MC-38621"/> <group value="importExport"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml index 075b7a5d06625..0ac035725179e 100644 --- a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml +++ b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml @@ -20,6 +20,7 @@ <testCaseId value="MC-38621"/> <group value="importExport"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Theme/Block/Html/Footer.php b/app/code/Magento/Theme/Block/Html/Footer.php index 7f9b9cf86a809..672e176b5da89 100644 --- a/app/code/Magento/Theme/Block/Html/Footer.php +++ b/app/code/Magento/Theme/Block/Html/Footer.php @@ -93,7 +93,7 @@ public function getCopyright() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } - return __($this->_copyright); + return $this->replaceCurrentYear((string)__($this->_copyright)); } /** @@ -133,4 +133,14 @@ protected function getCacheLifetime() { return parent::getCacheLifetime() ?: 3600; } + + /** + * Replace YYYY with the current year + * + * @param string $text + */ + private function replaceCurrentYear(string $text): string + { + return str_replace('{YYYY}', (new \DateTime())->format('Y'), $text); + } } diff --git a/app/code/Magento/Theme/Block/Html/Header.php b/app/code/Magento/Theme/Block/Html/Header.php index 95c146253bae3..1550ebaa367dc 100644 --- a/app/code/Magento/Theme/Block/Html/Header.php +++ b/app/code/Magento/Theme/Block/Html/Header.php @@ -6,7 +6,11 @@ namespace Magento\Theme\Block\Html; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\ScopeInterface; /** * Html page header block @@ -14,7 +18,7 @@ * @api * @since 100.0.2 */ -class Header extends \Magento\Framework\View\Element\Template +class Header extends Template { /** * @var Escaper @@ -22,19 +26,17 @@ class Header extends \Magento\Framework\View\Element\Template private $escaper; /** - * Constructor - * - * @param \Magento\Framework\View\Element\Template\Context $context - * @param Magento\Framework\Escaper $escaper + * @param Context $context * @param array $data + * @param Escaper|null $escaper */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - \Magento\Framework\Escaper $escaper, - array $data = [] + Context $context, + array $data = [], + Escaper $escaper = null ) { - $this->escaper = $escaper; parent::__construct($context, $data); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); } /** @@ -54,7 +56,7 @@ public function getWelcome() if (empty($this->_data['welcome'])) { $this->_data['welcome'] = $this->_scopeConfig->getValue( 'design/header/welcome', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } return $this->escaper->escapeQuote(__($this->_data['welcome'])->render(), true); diff --git a/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php b/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php index d9d2c0e041e99..f1feac56cfba4 100644 --- a/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php +++ b/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php @@ -42,6 +42,7 @@ public function __construct( /** * Change value from theme_full_path (Ex. "frontend/Magento/blank") to theme_id field for every existed scope. + * * All other values leave without changes. * * @param array $config @@ -51,10 +52,10 @@ public function process(array $config) { foreach ($config as $scope => &$item) { if ($scope === \Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { - $item = $this->changeThemeFullPathToIdentifier($item); + $item = $this->changeThemeFullPathToIdentifier($item ?? []); } else { foreach ($item as &$scopeItems) { - $scopeItems = $this->changeThemeFullPathToIdentifier($scopeItems); + $scopeItems = $this->changeThemeFullPathToIdentifier($scopeItems ?? []); } } } @@ -63,13 +64,12 @@ public function process(array $config) } /** - * Check \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID config path - * and convert theme_full_path (Ex. "frontend/Magento/blank") to theme_id + * Convert theme_full_path from config (Ex. "frontend/Magento/blank") to theme_id. * - * @param array $configItems - * @return array + * @see \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID + * @param array $configItems complete store configuration for a single scope as nested array */ - private function changeThemeFullPathToIdentifier($configItems) + private function changeThemeFullPathToIdentifier(array $configItems): array { $theme = null; $themeIdentifier = $this->arrayManager->get(DesignInterface::XML_PATH_THEME_ID, $configItems); diff --git a/app/code/Magento/Theme/README.md b/app/code/Magento/Theme/README.md index 9035df6395263..4bd55394389a3 100644 --- a/app/code/Magento/Theme/README.md +++ b/app/code/Magento/Theme/README.md @@ -1 +1 @@ -The Theme module contains common infrastructure that provides an ability to apply and use themes in Magento application. \ No newline at end of file +The Theme module contains common infrastructure that provides an ability to apply and use themes in Magento application. diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml index 056b4c3f914fe..ddf930b0ea804 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="https://github.com/magento/magento2/pull/25926"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml index 5cfc06664b393..a96c79d9f69aa 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml index 167191ee69a79..7be106ea58e72 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14112"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml index 87a0e0141f911..a0af380d25902 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-13832"/> <group value="Content"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml index 69673fa5e6daf..f0725bb8b164c 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25636"/> <group value="Watermark"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml index 76cfaa461dfaa..8d35de6c135ec 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml @@ -29,7 +29,7 @@ <after> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml index 0f9ff05af3d89..56947a6d713fe 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-91409"/> <group value="Theme"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php index 7682c83e0d38d..8a8cbbe8b458f 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php @@ -8,9 +8,14 @@ namespace Magento\Theme\Test\Unit\Block\Html; use Magento\Cms\Model\Block; +use Magento\Framework\App\Config; +use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Theme\Block\Html\Footer; +use Magento\Theme\Block\Html\Header; use PHPUnit\Framework\TestCase; class FooterTest extends TestCase @@ -20,10 +25,31 @@ class FooterTest extends TestCase */ protected $block; + /** + * @var Config + */ + private $scopeConfig; + protected function setUp(): void { $objectManager = new ObjectManager($this); - $this->block = $objectManager->getObject(Footer::class); + + $context = $this->getMockBuilder(Context::class) + ->setMethods(['getScopeConfig']) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeConfig = $this->getMockBuilder(Config::class) + ->setMethods(['getValue']) + ->disableOriginalConstructor() + ->getMock(); + $context->expects($this->once())->method('getScopeConfig')->willReturn($this->scopeConfig); + + $this->block = $objectManager->getObject( + Footer::class, + [ + 'context' => $context, + ] + ); } protected function tearDown(): void @@ -31,6 +57,17 @@ protected function tearDown(): void $this->block = null; } + public function testGetCopyright() + { + $this->scopeConfig->expects($this->once())->method('getValue') + ->with('design/footer/copyright', ScopeInterface::SCOPE_STORE) + ->willReturn('Copyright 2013-{YYYY}'); + + $this->assertEquals( + 'Copyright 2013-' . date('Y'), + $this->block->getCopyright() + ); + } public function testGetIdentities() { $this->assertEquals( diff --git a/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php b/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php index 574e553b2b1f4..c77972a009e84 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php @@ -12,6 +12,7 @@ use Magento\Framework\App\State; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager\TestProvidesServiceInterface; use Magento\Framework\View\Design\Theme\CustomizationFactory; use Magento\Framework\View\Design\Theme\CustomizationInterface; use Magento\Framework\View\Design\Theme\Domain\Factory; @@ -29,7 +30,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ThemeTest extends TestCase +class ThemeTest extends TestCase implements TestProvidesServiceInterface { /** * @var Theme|MockObject @@ -102,7 +103,6 @@ protected function setUp(): void $this->themeModelFactory = $this->createPartialMock(ThemeFactory::class, ['create']); $this->validator = $this->createMock(Validator::class); $this->appState = $this->createMock(State::class); - $objectManagerHelper = new ObjectManager($this); $arguments = $objectManagerHelper->getConstructArguments( Theme::class, @@ -118,10 +118,20 @@ protected function setUp(): void 'themeModelFactory' => $this->themeModelFactory ] ); - $this->_model = $objectManagerHelper->getObject(Theme::class, $arguments); } + /** + * @inheritdoc + */ + public function getServiceForObjectManager(string $type) : ?object + { + if (Collection::class == $type) { + return $this->resourceCollection; + } + return null; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Theme/i18n/en_US.csv b/app/code/Magento/Theme/i18n/en_US.csv index ac5d5c4a62cb9..6e797b1bfff59 100644 --- a/app/code/Magento/Theme/i18n/en_US.csv +++ b/app/code/Magento/Theme/i18n/en_US.csv @@ -173,6 +173,7 @@ Header,Header Footer,Footer "This will be displayed just before the body closing tag.","This will be displayed just before the body closing tag." "Miscellaneous HTML","Miscellaneous HTML" +"Use {YYYY} to insert the current year (updates on cache refresh).","Use {YYYY} to insert the current year (updates on cache refresh)." Copyright,Copyright "Default Robots","Default Robots" "Edit custom instruction of robots.txt File","Edit custom instruction of robots.txt File" diff --git a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml index a01805bd2e49c..e308caf835878 100644 --- a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml @@ -242,6 +242,7 @@ <validation> <rule name="validate-no-html-tags" xsi:type="boolean">true</rule> </validation> + <notice translate="true">Use {YYYY} to insert the current year (updates on cache refresh).</notice> <dataType>text</dataType> <label translate="true">Copyright</label> <dataScope>footer_copyright</dataScope> diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index f863da70e8987..1ef50f0cacfe4 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -5,8 +5,10 @@ */ ?> <div data-bind="scope: 'messages'"> - <!-- ko if: cookieMessages && cookieMessages.length > 0 --> - <div aria-atomic="true" role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> + <!-- ko if: cookieMessagesObservable() && cookieMessagesObservable().length > 0 --> + <div aria-atomic="true" role="alert" class="messages" data-bind="foreach: { + data: cookieMessagesObservable(), as: 'message' + }"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js index 388166b2b1663..5e574e342114d 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js @@ -19,6 +19,7 @@ define([ return Component.extend({ defaults: { cookieMessages: [], + cookieMessagesObservable: [], messages: [], allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] }, @@ -27,9 +28,18 @@ define([ * Extends Component object by storage observable messages. */ initialize: function () { - this._super(); + this._super().observe( + [ + 'cookieMessagesObservable' + ] + ); + // The "cookieMessages" variable is not used anymore. It exists for backward compatibility; to support + // merchants who have overwritten "messages.phtml" which would still point to cookieMessages instead of the + // observable variant (also see https://github.com/magento/magento2/pull/37309). this.cookieMessages = _.unique($.cookieStorage.get('mage-messages'), 'text'); + this.cookieMessagesObservable(this.cookieMessages); + this.messages = customerData.get('messages').extend({ disposableCustomerData: 'messages' }); diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 4a3ca10f56f82..c333149960c88 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -338,7 +338,7 @@ <!-- Go to next step --> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethodBeforeTranslate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStepBeforeTranslate"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStepBeforeTranslate"/> <!-- Check Progress Bar Review & Payments --> <waitForElementVisible selector="{{InlineTranslationModeCheckoutSection.progressBarActive}}" stepKey="waitForProgressBarReviewAndPayments"/> @@ -570,7 +570,7 @@ <!-- Go to next step --> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStep"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> <!-- Check translate Progress Bar Review & Payments--> <see userInput="Review & Payments Translated" selector="{{InlineTranslationModeCheckoutSection.progressBarActive}}" stepKey="seeTranslateProgressBarReviewAndPayments"/> diff --git a/app/code/Magento/Ui/README.md b/app/code/Magento/Ui/README.md index b7dd1a858e4a8..cc3105258bbd6 100644 --- a/app/code/Magento/Ui/README.md +++ b/app/code/Magento/Ui/README.md @@ -1,12 +1,15 @@ # Overview + ## Purpose of module The Magento\Ui module introduces a set of common UI components, which could be used and configured via layout XML files. # Deployment + ## System requirements The Magento\Ui module does not have any specific system requirements. ## Install + The Magento\Ui module is installed automatically (using the native Magento Setup). No additional actions required. diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml index 8c10f7a3dae88..0b71aaf85e308 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml @@ -20,6 +20,8 @@ <element name="cancelFilters" type="button" selector="button[data-action='grid-filter-cancel']" timeout="30"/> <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="clearFilters" type="button" selector=".admin__data-grid-header [data-action='grid-filter-reset']" timeout="30"/> + <element name="dateFilterFrom" type="input" selector="//input[@name='created_at[from]']"/> + <element name="dateFilterTo" type="input" selector="//input[@name='created_at[to]']"/> <!--Grid view bookmarks--> <element name="bookmarkToggle" type="button" selector="div.admin__data-grid-action-bookmarks button[data-bind='toggleCollapsible']" timeout="30"/> <element name="bookmarkToggleByIndex" type="button" selector="(//div[contains(@class,'admin__data-grid-action-bookmarks')])[{{index}}]//button[@data-bind='toggleCollapsible']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml index fe4069f0f28e5..40def5a146817 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml @@ -15,6 +15,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-37450"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js index 09f5674b7adb8..3c3c002603f29 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js @@ -136,9 +136,8 @@ define([ drEl.instance = recordNode = this.processingStyles(recordNode, elem); drEl.instanceCtx = this.getRecord(originRecord[0]); drEl.eventMousedownY = this.getPageY(event); - drEl.minYpos = - $table.offset().top - originRecord.offset().top + outerHight; - drEl.maxYpos = drEl.minYpos + $table.children('tbody').outerHeight() - originRecord.outerHeight(); + drEl.minYpos = $table.offset().top - originRecord.offset().top + outerHight; + drEl.maxYpos = drEl.minYpos + ($table.children('tbody').outerHeight() || 0) - originRecord.outerHeight(); $tableWrapper.append(recordNode); this.body.on('mousemove touchmove', this.mousemoveHandler); this.body.on('mouseup touchend', this.mouseupHandler); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index e7dc245d47d6f..988ff2ffe76f5 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -75,7 +75,14 @@ define([ * @returns {FileUploader} Chainable. */ setInitialValue: function () { - var value = this.getInitialValue(); + var value = this.getInitialValue(), + imageSize = this.setImageSize; + + _.each(value, function (val) { + if (val.type !== undefined && val.type.indexOf('image') >= 0) { + imageSize(val); + } + }, this); value = value.map(this.processFile, this); @@ -88,6 +95,19 @@ define([ return this; }, + /** + * Set image size for already loaded image + * + * @param value + * @returns {Promise<void>} + */ + async setImageSize(value) { + let response = await fetch(value.url), + blob = await response.blob(); + + value.size = blob.size; + }, + /** * Empties files list. * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js index 56d524290280c..c8d11b6cdf372 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js @@ -186,6 +186,10 @@ define([ delay = this.cachedRequestDelay, result; + if (request.showTotalRecords === undefined) { + request.showTotalRecords = true; + } + result = { items: this.getByIds(request.ids), totalRecords: request.totalRecords, @@ -215,6 +219,10 @@ define([ this.removeRequest(cached); } + if (data.showTotalRecords === undefined) { + data.showTotalRecords = true; + } + this._requests.push({ ids: this.getIds(data.items), params: params, diff --git a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml index 58ac4ef53861c..f61e031e8a7a1 100644 --- a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml +++ b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-99012"/> <useCaseId value="MAGETWO-98947"/> <group value="ups"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php index c508e85d87c3b..4ef7ea4b2062f 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php @@ -97,17 +97,51 @@ protected function getTargetPath($model) ]; $rewrite = $this->urlFinder->findOneByData($data); if (!$rewrite) { - $message = $model->getEntityType() === self::ENTITY_TYPE_PRODUCT - ? __("The selected product isn't associated with the selected store or category.") - : __("The selected category isn't associated with the selected store."); - throw new LocalizedException($message); + $model->getEntityType() === self::ENTITY_TYPE_PRODUCT ? $this->checkProductCorrelation($model) : + $this->checkCategoryCorrelation($model); + } else { + $targetPath = $rewrite->getRequestPath(); } - $targetPath = $rewrite->getRequestPath(); } return $targetPath; } /** + * Checks if rewrite details match category properties + * + * @param \Magento\UrlRewrite\Model\UrlRewrite $model + * @return void + * @throws LocalizedException + */ + private function checkCategoryCorrelation(\Magento\UrlRewrite\Model\UrlRewrite $model): void + { + if (false === in_array($model->getStoreId(), $this->_getCategory()->getStoreIds())) { + throw new LocalizedException( + __("The selected category isn't associated with the selected store.") + ); + } + } + + /** + * Checks if rewrite details match product properties + * + * @param \Magento\UrlRewrite\Model\UrlRewrite $model + * @return void + * @throws LocalizedException + */ + private function checkProductCorrelation(\Magento\UrlRewrite\Model\UrlRewrite $model): void + { + if (false === ($this->_getProduct()->canBeShowInCategory($this->_getCategory()->getId())) && + in_array($model->getStoreId(), $this->_getProduct()->getStoreIds())) { + throw new LocalizedException( + __("The selected product isn't associated with the selected store or category.") + ); + } + } + + /** + * Get rewrite canonical target path + * * @return string */ protected function getCanonicalTargetPath() @@ -142,6 +176,8 @@ private function _handleCmsPageUrlRewrite($model) } /** + * Process save URL rewrite request + * * @return void */ public function execute() diff --git a/app/code/Magento/UrlRewrite/Model/UrlRewrite.php b/app/code/Magento/UrlRewrite/Model/UrlRewrite.php index 134e27a6efd99..b5b2d5ffa52b4 100644 --- a/app/code/Magento/UrlRewrite/Model/UrlRewrite.php +++ b/app/code/Magento/UrlRewrite/Model/UrlRewrite.php @@ -14,6 +14,7 @@ use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; @@ -40,7 +41,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ -class UrlRewrite extends AbstractModel +class UrlRewrite extends AbstractModel implements ResetAfterRequestInterface { /** * @var Json @@ -235,4 +236,12 @@ public function afterSave() $this->_getResource()->addCommitCallback([$this, 'cleanEntitiesCache']); return parent::afterSave(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->entityToCacheTagMap = []; + } } diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml index 3692e82072afc..404fcb52c91e9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml @@ -28,7 +28,7 @@ </entity> <entity name="customPermanentUrlRewrite" type="urlRewrite"> <data key="request_path" unique="prefix">wishlist</data> - <data key="target_path">https://marketplace.magento.com/</data> + <data key="target_path">https://commercemarketplace.adobe.com/</data> <data key="redirect_type">301</data> <data key="redirect_type_label">Permanent (301)</data> <data key="store_id">1</data> @@ -37,7 +37,7 @@ </entity> <entity name="customTemporaryUrlRewrite" type="urlRewrite"> <data key="request_path" unique="prefix">wishlist</data> - <data key="target_path">https://marketplace.magento.com/</data> + <data key="target_path">https://commercemarketplace.adobe.com/</data> <data key="redirect_type">302</data> <data key="redirect_type_label">Temporary (302)</data> <data key="store_id">1</data> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductCategoryPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductCategoryPage.xml new file mode 100644 index 0000000000000..33db1aff8f551 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductCategoryPage.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="AdminUrlRewriteProductCategoryPage" url="admin/url_rewrite/edit/product/{{productId}}/category/{{categoryId}}" area="admin" module="Magento_UrlRewrite"> + <section name="AdminUrlRewriteProductCategorySection"/> + </page> +</pages> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml index b6a155c9db6fb..03cc49f95e63a 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -15,5 +15,6 @@ <element name="gridCellByColumnValue" type="text" selector="//*[@data-role='grid']//tbody//td[count(//*[@data-role='grid']//th[contains(., '{{column}}')]/preceding-sibling::th)+1][normalize-space(.)='{{columnValue}}']" parameterized="true"/> <element name="select" type="button" selector="//*[@data-role='grid']//tbody//tr[{{row}}+1]//button[@class='action-select']" timeout="30" parameterized="true"/> <element name="activeEdit" type="button" selector="//*[@data-role='grid']//tbody//ul[@class='action-menu _active']//a[@data-action='item-edit']" timeout="30"/> + <element name="clearFiltersButton" type="button" selector="//div[@class='admin__data-grid-header']//button[@class='action-tertiary action-clear']" timeout="10"/> </section> </sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml index 795a3e956cf82..8402c546297ab 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5342"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index 8e4fc536a898b..a04ab40bb26bc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6802"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <before> <!-- Set the configuration for Generate "category/product" URL Rewrites to Yes (default)--> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml index 7f82cbd506f20..4d708931e46e1 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5335"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml index f0ac774756958..1fe83a60ad8c5 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5334"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml index 5409a669211df..63c242a0810f9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5336"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml index ec1a7586722ff..aba476ea1e6bb 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5345"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml index d3728cc58b775..5bf3eec687a29 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5346"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml index a3ca2c84ceffb..d6f287c796f5a 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5343"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml index c007399faa654..b68c4ece60844 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5344"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml index bec16ec89ed25..6d1cbfb51ec51 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5338"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml index 1a47130b14858..b9242b7977a8c 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5341"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml index b6f3c8691e563..8dab428af2528 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5340"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index d41be98632aa3..36d2e9f2f61dc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageNoRedirectUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageNoRedirectUrlRewriteTest.xml index 19eaa0029c39a..07c341c15ae4f 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageNoRedirectUrlRewriteTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageNoRedirectUrlRewriteTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPagePermanentRedirectUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPagePermanentRedirectUrlRewriteTest.xml index 8bf9516720f90..45049b84396b2 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPagePermanentRedirectUrlRewriteTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPagePermanentRedirectUrlRewriteTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageTemporaryRedirectUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageTemporaryRedirectUrlRewriteTest.xml index 91bdd4a22059c..c9205ff1816f6 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageTemporaryRedirectUrlRewriteTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCMSPageTemporaryRedirectUrlRewriteTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml index 6dddc0f333ca5..735facf3b52cc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml index 8113d4511d8da..b640b67c536f3 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithNoRedirectsTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithNoRedirectsTest.xml index 1d582582684fa..c073ab8361706 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithNoRedirectsTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithNoRedirectsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14648"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleCmsPage" stepKey="createCMSPage"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithPermanentRedirectTest.xml index bd51fc9ce81d1..23889102c47cd 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithPermanentRedirectTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14649"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleCmsPage" stepKey="createCMSPage"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithTemporaryRedirectTest.xml index c1deef78fcd7a..d363144fe19b3 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCmsPageUrlRewriteWithTemporaryRedirectTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14650"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleCmsPage" stepKey="createCMSPage"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCreateProductUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCreateProductUrlRewriteTest.xml new file mode 100644 index 0000000000000..eacf142f66a23 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCreateProductUrlRewriteTest.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="AdminDeleteCreateProductUrlRewriteTest"> + <annotations> + <stories value="Delete Product and product-category URL rewrites and then create a new one"/> + <title value="Delete Product and product-category URL rewrites and then create a new one"/> + <description value="Delete automated URL rewrites and then create one"/> + <testCaseId value="AC-8380"/> + <severity value="MAJOR"/> + <group value="url_rewrite"/> + </annotations> + <before> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableCategoryProductRewrites"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!-- Create the category to put the product in --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="performReindex"/> + <magentoCLI command="cache:flush" stepKey="cleanCache"/> + </before> + <after> + <!-- Clear filters: URL re-write, product --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteEditPage"/> + <conditionalClick selector="{{AdminUrlRewriteIndexSection.clearFiltersButton}}" dependentSelector="{{AdminUrlRewriteIndexSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <!-- Delete the category and product --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> + <magentoCLI command="indexer:reindex" stepKey="performExitReindex"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Delete created product url rewrite and verify AssertUrlRewriteDeletedMessage--> + <actionGroup ref="AdminDeleteUrlRewriteActionGroup" stepKey="deleteProductUrlRewrite"> + <argument name="requestPath" value="$$createSimpleProduct.custom_attributes[url_key]$$.html"/> + </actionGroup> + <actionGroup ref="AdminDeleteUrlRewriteActionGroup" stepKey="deleteProductCategoryUrlRewrite"> + <argument name="requestPath" value="$$createSimpleProduct.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!--Search and verify AssertUrlRewriteNotInGrid--> + <actionGroup ref="AdminSearchDeletedUrlRewriteActionGroup" stepKey="searchDeletedUrlRewriteInGrid"> + <argument name="requestPath" value="$$createSimpleProduct.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTreeActionGroup" stepKey="getCategoryId"> + <argument name="category" value="$$createCategory.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Create product redirect --> + <amOnPage url="{{AdminUrlRewriteProductCategoryPage.url({$productId}, {$categoryId})}}" stepKey="openProductRedirectWithCategory"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('Temporary (302)')}}" stepKey="clickOnRedirectTypeValue"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + + <!-- Assert Url Rewrite Save Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="The URL Rewrite has been saved."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml index 293c412570742..5d8c186acd131 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteProductURLRewriteEntityTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteProductURLRewriteEntityTest.xml index 4ad6e99b0b424..132c6b34e9a56 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteProductURLRewriteEntityTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteProductURLRewriteEntityTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml index e6ac90a32d0c1..cc8e69db523e1 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14202"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml index 24620f7a7e763..09c4d84447c1f 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="catalog"/> <group value="url_rewrite"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml index ef43020191c4c..57862c413abe6 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5358"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml index ab911e95dfbc3..72a7f0dc66189 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5355"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml index 2222267db0e9f..929a32c98a3b8 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5357"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml index 9cc8530959559..da333ab2f1857 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5356"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml index edf4eab6bfcd2..8c99a0bb2deff 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml @@ -15,6 +15,7 @@ <severity value="MINOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml index e1f9d189f03a6..8406afbc92a09 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml index 525eec317d2be..5bfaba6193b5c 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <severity value="MINOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddNoRedirectTest.xml index 03a4e1fc7d82f..3a63bc096f640 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddNoRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddNoRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddPermanentRedirectTest.xml index 527ba1bfea065..82b352734002c 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddPermanentRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddTemporaryRedirectTest.xml index bd3766a8c51a1..73e0bd6e60b23 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value=""/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml index 16916426167b8..99f02c6df9440 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml index f450d7640d360..9055abe225187 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="urlRewrite"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml index ea97175d8823c..42cbd078abfc3 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-5351"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml index bc2005b32bae2..ba3a55f9d7023 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6964"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <before> <remove keyForRemoval="createSimpleProduct"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml index 06d54b10c1402..90ff84b3fc9f9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6844"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <!-- Preconditions--> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml index fc380f433bfbc..a7b0fd5c963f9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94803"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <before> <!-- Set the configuration for Generate "category/product" URL Rewrites to Yes (default)--> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/VerifyCategoryTreeOnAddUrlRewritePageTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/VerifyCategoryTreeOnAddUrlRewritePageTest.xml new file mode 100644 index 0000000000000..045579af449e5 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/VerifyCategoryTreeOnAddUrlRewritePageTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyCategoryTreeOnAddUrlRewritePageTest"> + <annotations> + <stories value="Create category URL rewrite"/> + <title value="Valid category tree on the Add URL Rewrite page"/> + <description value="Validating a category tree while creating category URL rewrites"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8310"/> + <useCaseId value="ACP2E-1703"/> + <group value="urlRewrite"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!-- Create six level nested category --> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> + <requiredEntity createDataKey="createTwoLevelNestedCategories"/> + </createData> + <createData entity="Four_nested_categories" stepKey="createFourLevelNestedCategories"> + <requiredEntity createDataKey="createThreeLevelNestedCategories"/> + </createData> + <createData entity="Five_nested_categories" stepKey="createFiveLevelNestedCategories"> + <requiredEntity createDataKey="createFourLevelNestedCategories"/> + </createData> + <createData entity="Six_nested_categories" stepKey="createSixLevelNestedCategories"> + <requiredEntity createDataKey="createFiveLevelNestedCategories"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSixLevelNestedCategories" stepKey="deleteSixNestedCategories"/> + <deleteData createDataKey="createFiveLevelNestedCategories" stepKey="deleteFiveNestedCategories"/> + <deleteData createDataKey="createFourLevelNestedCategories" stepKey="deleteFourNestedCategories"/> + <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> + <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminGoToAddNewUrlRewritePageActionGroup" stepKey="openUrlRewriteEditPage"/> + <actionGroup ref="AdminCreateNewUrlRewriteForCmsPageActionGroup" stepKey="selectForCategoryType"> + <argument name="customUrlRewriteValue" value="For Category"/> + </actionGroup> + <executeJS stepKey="getCategoryTreeLevelsCount" function="return document.querySelectorAll("li[id='$createDefaultCategory.id$'] ul").length;"/> + <assertEquals message="Asserting category levels count" stepKey="assertCategoryTreeLevelsCount"> + <expectedResult type="string">5</expectedResult> + <actualResult type="string">{$getCategoryTreeLevelsCount}</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/templates/categories.phtml b/app/code/Magento/UrlRewrite/view/adminhtml/templates/categories.phtml index bccffcef87867..38ff5c5b1edd6 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/templates/categories.phtml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/templates/categories.phtml @@ -5,23 +5,23 @@ */ /** @var \Magento\UrlRewrite\Block\Catalog\Category\Tree $block */ +$root = $block->getRoot(null, 0) ?> <fieldset class="admin__fieldset" data-ui-id="category-selector"> <legend class="admin__legend"><span><?= $block->escapeHtml(__('Select Category')) ?></span></legend> <div class="content content-category-tree"> <input type="hidden" name="categories" id="product_categories" value=""/> - <?php if ($block->getRoot()) : ?> + <?php if ($root): ?> <div class="jstree-default"></div> <?php endif; ?> </div> </fieldset> -<?php if ($block->getRoot()) : ?> +<?php if ($root): ?> <script type="text/x-magento-init"> { ".jstree-default": { "categoryTree": { - "data": <?= /* @noEscape */ - $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getTreeArray()); ?>, + "data": <?= /* @noEscape */ $block->getTreeArray(null, true); ?>, "url": "<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())); ?>" } } diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml index 813e22df227c8..b05581c04e3a9 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml @@ -14,7 +14,7 @@ <amOnPage url="{{AdminRolesPage.url}}" stepKey="amOnAdminUsersPage"/> <waitForPageLoad stepKey="waitForUserRolePageLoad"/> <click stepKey="clickToAddNewRole" selector="{{AdminDeleteRoleSection.role(role.name)}}"/> - <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/> <click stepKey="clickToDeleteRole" selector="{{AdminDeleteRoleSection.delete}}"/> <waitForElementVisible stepKey="wait" selector="{{AdminDeleteRoleSection.confirm}}" time="30"/> <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteNewUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteNewUserActionGroup.xml index a4e5492f8e3e6..a1410213daa14 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteNewUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteNewUserActionGroup.xml @@ -16,7 +16,7 @@ <argument name="userName" type="string" defaultValue="John"/> </arguments> <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser(userName)}}"/> - <fillField stepKey="typeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <fillField stepKey="typeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> <waitForPageLoad stepKey="waitForDeletePopupOpen" time="5"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml index 82a3a37cdd724..658cc405dadf8 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml @@ -18,6 +18,13 @@ <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> + <!-- Add reset filter to locate the required user as user page has more than 1 pages --> + <click selector="{{AdminUserGridSection.resetButton}}" stepKey="resetGridFilter"/> + <waitForPageLoad stepKey="waitForFiltersReset" time="15"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{user.username}}" stepKey="enterUserName"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForGridToLoad" time="15"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{user.username}}" stepKey="seeUser"/> <click stepKey="openTheUser" selector="{{AdminDeleteUserSection.role(user.username)}}"/> <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserViaCurlActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserViaCurlActionGroup.xml index 516fe9ebac09d..d1570fde6fc55 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserViaCurlActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserViaCurlActionGroup.xml @@ -14,10 +14,9 @@ <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> <click selector="{{AdminLegacyDataGridFilterSection.clear}}" stepKey="resetFilters" /> - <scrollTo selector="{{AdminLegacyDataGridTableSection.columnTemplateStrict(user.username, 'user_id')}}" stepKey="scrollToUserId"></scrollTo> - <waitForElementVisible selector="{{AdminLegacyDataGridTableSection.columnTemplateStrict(user.username, 'user_id')}}" stepKey="waitForUserIdVisible"></waitForElementVisible> + <waitForElementVisible selector="{{AdminLegacyDataGridTableSection.columnTemplateStrict(user.username, 'user_id')}}" stepKey="waitForUserIdVisible" /> + <scrollTo selector="{{AdminLegacyDataGridTableSection.columnTemplateStrict(user.username, 'user_id')}}" stepKey="scrollToUserId" /> <grabTextFrom selector="{{AdminLegacyDataGridTableSection.columnTemplateStrict(user.username, 'user_id')}}" stepKey="userId" /> - <!-- @TODO: Remove "executeJS" in scope of MQE-1561 Hack to be able to pass current admin user password without hardcoding it. diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml index 2c4ba807f047d..0e878f0a1857c 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml @@ -18,6 +18,7 @@ <group value="AsynchronousOperations"/> <group value="User"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml index 2d601b32fe8f1..dc339cbd1b881 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml @@ -19,6 +19,7 @@ <group value="user"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml index c26821d5be4b2..c7f5e6965a1ab 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="user"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="adminMainLogin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateUserRoleEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateUserRoleEntityTest.xml index b4a2a637aa003..68e89f0352002 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateUserRoleEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateUserRoleEntityTest.xml @@ -19,6 +19,7 @@ <group value="mtf_migrated"/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminDeleteAdminUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminDeleteAdminUserEntityTest.xml index a7617e9cb5996..6ae6b38ea8b40 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminDeleteAdminUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminDeleteAdminUserEntityTest.xml @@ -19,6 +19,7 @@ <group value="mtf_migrated"/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminDeleteOwnAdminUserAccountTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminDeleteOwnAdminUserAccountTest.xml index a0e6f7d43c516..294a6e337f315 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminDeleteOwnAdminUserAccountTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminDeleteOwnAdminUserAccountTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="user"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -34,8 +35,9 @@ <after> <!-- Delete New Admin User --> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAdmin"/> - <actionGroup ref="AdminDeleteUserViaCurlActionGroup" stepKey="deleteUser"> - <argument name="user" value="$$user$$" /> + <actionGroup ref="AdminOpenAdminUsersPageActionGroup" stepKey="goToAllUsersPage"/> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="$$user.username$$"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> </after> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml index 6241d970fdff3..31f50668b388d 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!-- First attempt to reset password --> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml index ad6ff8b8f0665..b1200e4ce3fb2 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml index 80fd5ef379b6e..9f73819f1986f 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml index 32036c2e82de3..c32fc7321f146 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml index 9c39ef6a898d9..4491c66f72cbb 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminUnlockAdminUserEntityViaCLITest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUnlockAdminUserEntityViaCLITest.xml index e20498d79291a..f6c0178face9f 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminUnlockAdminUserEntityViaCLITest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUnlockAdminUserEntityViaCLITest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="user"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set admin/captcha/enable 0" stepKey="disableAdminCaptcha"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserPasswordTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserPasswordTest.xml index 6d2c123a3ead4..8e81999f55050 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserPasswordTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserPasswordTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="user"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserRoleTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserRoleTest.xml index 9267fae8c1550..074bbe6025214 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserRoleTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserRoleTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-27895"/> <group value="user"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Usps/Model/Config/Backend/UspsUrl.php b/app/code/Magento/Usps/Model/Config/Backend/UspsUrl.php new file mode 100644 index 0000000000000..17cdcc83961bd --- /dev/null +++ b/app/code/Magento/Usps/Model/Config/Backend/UspsUrl.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Usps\Model\Config\Backend; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\Validator\Url; + +/** + * Represents a config URL that may point to a USPS endpoint + * + * @SuppressWarnings(PHPMD.Superglobals) + */ +class UspsUrl extends Value +{ + /** + * @var Url + */ + private Url $url; + + /** + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param Url $url + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + Url $url, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [] + ) { + $this->url = $url; + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + } + + /** + * @inheritdoc + * + * @throws ValidatorException + */ + public function beforeSave() + { + $isValid = $this->url->isValid($this->getValue()); + if ($isValid) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $host = parse_url((string)$this->getValue(), \PHP_URL_HOST); + + if (!empty($host) && !preg_match("/(?:.+\.|^)usps|shippingapis\.com$/i", $host)) { + throw new ValidatorException(__('USPS API endpoint URL\'s must use usps.com or shippingapis.com')); + } + } + + return parent::beforeSave(); + } +} diff --git a/app/code/Magento/Usps/Test/Unit/Model/Config/Backend/UspsUrlTest.php b/app/code/Magento/Usps/Test/Unit/Model/Config/Backend/UspsUrlTest.php new file mode 100644 index 0000000000000..da18923366c55 --- /dev/null +++ b/app/code/Magento/Usps/Test/Unit/Model/Config/Backend/UspsUrlTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Usps\Test\Unit\Model\Config\Backend; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Validator\Url; +use Magento\Usps\Model\Config\Backend\UspsUrl; +use PHPUnit\Framework\TestCase; + +/** + * Verify behavior of UspsUrl backend type + * + * @SuppressWarnings(PHPMD.Superglobals) + */ +class UspsUrlTest extends TestCase +{ + /** + * @var UspsUrl + */ + private $urlConfig; + + /** + * @var Url + */ + private $url; + + /** + * @var Context + */ + private $contextMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->contextMock = $this->createMock(Context::class); + $registry = $this->createMock(Registry::class); + $config = $this->createMock(ScopeConfigInterface::class); + $cacheTypeList = $this->createMock(TypeListInterface::class); + $this->url = $this->createMock(Url::class); + $resource = $this->createMock(AbstractResource::class); + $resourceCollection = $this->createMock(AbstractDb::class); + $eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + + $eventManagerMock->expects($this->any())->method('dispatch'); + $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($eventManagerMock); + + $this->urlConfig = $objectManager->getObject( + UspsUrl::class, + [ + 'url' => $this->url, + 'context' => $this->contextMock, + 'registry' => $registry, + 'config' => $config, + 'cacheTypeList' => $cacheTypeList, + 'resource' => $resource, + 'resourceCollection' => $resourceCollection, + ] + ); + } + + /** + * @dataProvider validDataProvider + * @param string $data The valid data + * @throws ValidatorException + */ + public function testBeforeSave(string $data = ""): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + $this->assertTrue($this->url->isValid($data)); + } + + /** + * @dataProvider invalidDataProvider + * @param string $data The invalid data + */ + public function testBeforeSaveErrors(string $data): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->expectException('Magento\Framework\Exception\ValidatorException'); + $this->expectExceptionMessage('USPS API endpoint URL\'s must use usps.com or shippingapis.com'); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + } + + public function validDataProvider(): array + { + return [ + [], + [''], + ['http://usps.com'], + ['https://foo.usps.com'], + ['http://foo.usps.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } + + /** + * @return string[][] + */ + public function invalidDataProvider(): array + { + return [ + ['https://shippingapis.com.fake.com'], + ['https://shippingapis.info'], + ['http://shippingapis.com.foo.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } +} diff --git a/app/code/Magento/Usps/etc/adminhtml/system.xml b/app/code/Magento/Usps/etc/adminhtml/system.xml index b01f7be9a19f9..00c9632b99367 100644 --- a/app/code/Magento/Usps/etc/adminhtml/system.xml +++ b/app/code/Magento/Usps/etc/adminhtml/system.xml @@ -16,9 +16,11 @@ </field> <field id="gateway_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Gateway URL</label> + <backend_model>Magento\Usps\Model\Config\Backend\UspsUrl</backend_model> </field> <field id="gateway_secure_url" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Secure Gateway URL</label> + <backend_model>Magento\Usps\Model\Config\Backend\UspsUrl</backend_model> </field> <field id="title" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Title</label> diff --git a/app/code/Magento/Usps/i18n/en_US.csv b/app/code/Magento/Usps/i18n/en_US.csv index ab1a11155fe04..65837cfb4dc77 100644 --- a/app/code/Magento/Usps/i18n/en_US.csv +++ b/app/code/Magento/Usps/i18n/en_US.csv @@ -137,3 +137,4 @@ Machinable,Machinable Debug,Debug "Show Method if Not Applicable","Show Method if Not Applicable" "Sort Order","Sort Order" +"USPS API endpoint URL\'s must use usps.com or shippingapis.com","USPS API endpoint URL\'s must use usps.com or shippingapis.com" diff --git a/app/code/Magento/Variable/Test/Mftf/Test/AdminCreateCustomVariableEntityTest.xml b/app/code/Magento/Variable/Test/Mftf/Test/AdminCreateCustomVariableEntityTest.xml index 1c6e222a242d8..80da7dfefb5d6 100644 --- a/app/code/Magento/Variable/Test/Mftf/Test/AdminCreateCustomVariableEntityTest.xml +++ b/app/code/Magento/Variable/Test/Mftf/Test/AdminCreateCustomVariableEntityTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="variable"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml index 4e7abaa75a492..d204e4bdbe250 100644 --- a/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml +++ b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Variable/Test/Unit/Model/VariableTest.php b/app/code/Magento/Variable/Test/Unit/Model/VariableTest.php index d998062699f17..d709ea4e24caa 100644 --- a/app/code/Magento/Variable/Test/Unit/Model/VariableTest.php +++ b/app/code/Magento/Variable/Test/Unit/Model/VariableTest.php @@ -10,14 +10,15 @@ use Magento\Framework\Escaper; use Magento\Framework\Phrase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager\TestProvidesServiceInterface; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use Magento\Variable\Model\ResourceModel\Variable; use Magento\Variable\Model\ResourceModel\Variable\Collection; -use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Magento\Framework\Validation\ValidationException; -class VariableTest extends TestCase +class VariableTest extends TestCase implements TestProvidesServiceInterface { /** * @var \Magento\Variable\Model\Variable @@ -79,6 +80,17 @@ protected function setUp(): void $this->validationFailedPhrase = __('Validation has failed.'); } + /** + * @inheritdoc + */ + public function getServiceForObjectManager(string $type) : ?object + { + if (Collection::class == $type) { + return $this->resourceCollectionMock; + } + return null; + } + public function testGetValueHtml() { $type = \Magento\Variable\Model\Variable::TYPE_HTML; diff --git a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php index 892fe71ffe67f..89d48ff4c3c65 100644 --- a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php +++ b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php @@ -9,6 +9,7 @@ use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Integration\Api\Exception\UserTokenException; use Magento\Integration\Api\UserTokenReaderInterface; use Magento\Integration\Api\UserTokenValidatorInterface; @@ -22,8 +23,11 @@ /** * A user context determined by tokens in a HTTP request Authorization header. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ -class TokenUserContext implements UserContextInterface +class TokenUserContext implements UserContextInterface, ResetAfterRequestInterface { /** * @var Request @@ -78,7 +82,7 @@ class TokenUserContext implements UserContextInterface * @param UserTokenValidatorInterface|null $tokenValidator */ public function __construct( - Request $request, + \Magento\Framework\App\RequestInterface $request, TokenFactory $tokenFactory, IntegrationServiceInterface $integrationService, DateTime $dateTime = null, @@ -190,4 +194,14 @@ protected function setUserDataViaToken(Token $token) $this->userType = null; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isRequestProcessed = false; + $this->userId = null; + $this->userType = null; + } } diff --git a/app/code/Magento/Webapi/README.md b/app/code/Magento/Webapi/README.md index 26f7ad9f09ebd..cfa33741133f2 100644 --- a/app/code/Magento/Webapi/README.md +++ b/app/code/Magento/Webapi/README.md @@ -2,4 +2,4 @@ **Webapi** provides the framework for the application to expose REST and SOAP web services. It exposes an area for REST and another area for SOAP services and routes requests based on the Webapi configuration. It also handles -deserialization of requests and serialization of responses. +deserialization of requests and serialization of responses. diff --git a/app/code/Magento/Weee/README.md b/app/code/Magento/Weee/README.md index b3d59174a8337..64d6973e048cd 100644 --- a/app/code/Magento/Weee/README.md +++ b/app/code/Magento/Weee/README.md @@ -88,7 +88,7 @@ For more information about a layout, see the [Layout documentation](https://deve ### UI components -You can extend a customer form and widgets using the configuration files located in the directories +You can extend a customer form and widgets using the configuration files located in the directories - `view/adminhtml/ui_component`: - `product_attribute_add_form` diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AddingSeveralFPTToSimpleProductTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AddingSeveralFPTToSimpleProductTest.xml index 56e834ad39923..7ffe5ec96cb56 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AddingSeveralFPTToSimpleProductTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AddingSeveralFPTToSimpleProductTest.xml @@ -19,6 +19,7 @@ <group value="checkout"/> <group value="tax"/> <group value="weee"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 5d10a36d88eac..9b9811e5c820c 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-95033"/> <group value="weee"/> + <group value="cloud"/> </annotations> <before> <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTMultipleProductOrderTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTMultipleProductOrderTest.xml index 9e02f0678ff9a..84433863c2a50 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTMultipleProductOrderTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTMultipleProductOrderTest.xml @@ -93,7 +93,7 @@ <!-- Select shipping and payment methods --> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="clickCheckMoneyOrderPayment"/> <!-- Open summary section for product --> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 4b3cd5d4a99ed..77b7a33fb8f47 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -21,6 +21,7 @@ <group value="tax"/> <group value="weee"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index bb6fe082f4540..8313760d64f2a 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -20,6 +20,7 @@ <group value="checkout"/> <group value="tax"/> <group value="weee"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml index ac22ec9218401..1ad4fe78ddbbd 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml @@ -21,6 +21,7 @@ <group value="tax"/> <group value="weee"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php index d2ea44fff5bcc..0539ba3ec3aca 100644 --- a/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Weee\Helper\Data; use Magento\Tax\Helper\Data as TaxHelper; use Magento\Store\Api\Data\StoreInterface; @@ -19,7 +20,7 @@ /** * Resolver for the FPT store config settings */ -class StoreConfig implements ResolverInterface +class StoreConfig implements ResolverInterface, ResetAfterRequestInterface { /** * @var string @@ -61,6 +62,14 @@ public function __construct(Data $weeeHelper, TaxHelper $taxHelper) $this->taxHelper = $taxHelper; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->computedFptSettings = []; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Widget/README.md b/app/code/Magento/Widget/README.md index ee4594e1cef2b..4eff2e758b4c4 100644 --- a/app/code/Magento/Widget/README.md +++ b/app/code/Magento/Widget/README.md @@ -37,5 +37,5 @@ This module introduces the following layouts and layout handles in the directori - `view/frantend/layout`: - `default` - `print` - + For more information about a layout, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsDisplayOnSpecificEntitiesTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsDisplayOnSpecificEntitiesTest.xml index d41100b0fbefe..c561263533e42 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsDisplayOnSpecificEntitiesTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsDisplayOnSpecificEntitiesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="widget"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsMassDeletesTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsMassDeletesTest.xml index 6e83687207341..80afb4bb77160 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsMassDeletesTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsMassDeletesTest.xml @@ -16,6 +16,7 @@ <description value="Admin select widgets in grid and try to mass delete action, will show pop-up with confirm action"/> <severity value="MAJOR"/> <group value="widget"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml index fbf3e50b56ce3..7f2fb5763dca6 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml index 5e053778fe7ed..97f93887da2f1 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-37892"/> <group value="widget"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml index 79af80de7fc2a..f2a752550a54d 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="Widget"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/CheckDisplayingOfApostrophesInTheTextFieldBoxWhileCreatingPageWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/CheckDisplayingOfApostrophesInTheTextFieldBoxWhileCreatingPageWidgetTest.xml new file mode 100644 index 0000000000000..82b43a0756e17 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/CheckDisplayingOfApostrophesInTheTextFieldBoxWhileCreatingPageWidgetTest.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="CheckDisplayingOfApostrophesInTheTextFieldBoxWhileCreatingPageWidgetTest"> + <annotations> + <features value="Widget"/> + <stories value="Checking displaying of apostrophes (') in the text field box while creating page widget"/> + <title value="Checking displaying of apostrophes (') in the text field box while creating page widget"/> + <description value="Checking displaying of apostrophes (') in the text field box while creating page widget"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4389"/> + <group value="widget"/> + <group value="pagebuilder_disabled"/> + </annotations> + <before> + <!-- Pre-condition 1- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Pre-condition 3- Verify page in grid--> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="openCMSPagesGridActionGroup"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="SortByIdDescendingActionGroup" stepKey="sortGridByIdDescending"/> + <click selector="{{CmsPagesPageActionsSection.select('home')}}" stepKey="clickSelectCMSPage" /> + <!-- Pre-condition 4- Update the page in grid--> + <click selector="{{CmsPagesPageActionsSection.edit('home')}}" stepKey="OpenThePageToBeEdited"/> + <waitForPageLoad stepKey="waitForPageLoadPostSelectingHomePage"/> + <waitForElementVisible selector="{{CmsNewPagePageContentSection.header}}" stepKey="waitForContentTabForPageToBeVisible"/> + <!-- Pre-condition 5- Expand the Content section > Insert Widget --> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <conditionalClick selector="{{CmsNewPagePageActionsSection.showHideEditor}}" dependentSelector="{{CmsNewPagePageActionsSection.showHideEditor}}" visible="true" stepKey="clickOnShowHideEditorLinkIfVisibleForInsertingWidget"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.insertWidget}}" stepKey="waitForTheInsertWidgetButtonToDisplay"/> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="selectCMSPageLinkFromDropdown"/> + <waitForPageLoad stepKey="waitForPageLoadPostSelectingFromDropDown"/> + <!-- Pre-condition 6 - 11 - Update the Anchor Custom Text --> + <fillField selector="{{WidgetSection.InputAnchorCustomText}}" userInput="Custom texts' for tests" stepKey="InputValuesWithApostrophe"/> + <click selector="{{WidgetSection.SelectPageButton}}" stepKey="clickOnSelectPageButton"/> + <waitForElementVisible selector="{{WidgetSection.URLKeySelectPage}}" stepKey="waitForSelectPageDialogToPopulate"/> + <fillField selector="{{WidgetSection.URLKeySelectPage}}" userInput="home" stepKey="EnterThePageURL"/> + <click selector="{{WidgetSection.SearchButtonSelectPage}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForResultsToBeDisplayed"/> + <click selector="{{WidgetSection.SearchResultSelectPage('home')}}" stepKey="clickOnDisplayedResult"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickOnInsertWidget"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForInsertWidgetDialogToDisappear" time="5"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePage"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="waitForSuccessMessageLoggedOut" time="5"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + </before> + <after> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="openCMSPagesGridActionGroupToReset"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFiltersToReset"/> + <actionGroup ref="SortByIdDescendingActionGroup" stepKey="sortGridByIdDescendingToReset"/> + <click selector="{{CmsPagesPageActionsSection.select('home')}}" stepKey="clickSelectCMSPageToReset" /> + <!-- Pre-condition 4- Update the page in grid To Reset--> + <click selector="{{CmsPagesPageActionsSection.edit('home')}}" stepKey="OpenThePageToBeEditedToReset"/> + <waitForPageLoad stepKey="waitForPageLoadPostSelectingHomePageToReset"/> + <waitForElementVisible selector="{{CmsNewPagePageContentSection.header}}" stepKey="waitForContentTabForPageToBeVisibleToReset"/> + <!-- Pre-condition 5- Expand the Content section > Insert Widget To Reset --> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPageToReset"/> + <conditionalClick selector="{{CmsNewPagePageActionsSection.showHideEditor}}" dependentSelector="{{CmsNewPagePageActionsSection.showHideEditor}}" visible="true" stepKey="clickOnShowHideEditorLinkIfVisibleForInsertingWidgetToOriginal"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.insertWidget}}" stepKey="waitForTheInsertWidgetButtonToDisplayToReset"/> + <clearField selector="{{CmsNewPagePageContentSection.content}}" stepKey="clearWidgetTextFieldToReset"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="<p>CMS homepage content goes here.</p>" stepKey="InputDefaultValuesInWidgetTextFieldToReset"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForInsertWidgetDialogToDisappearToReset" time="5"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePageToReset"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="waitForSuccessMessageLoggedOutToReset" time="5"/> + <see userInput="You saved the page." stepKey="seeSuccessMessageToReset"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="navigateToHomePage"/> + <waitForPageLoad stepKey="waitToLoadHomePage"/> + <grabTextFrom selector="{{StorefrontCMSPageSection.widgetContentApostrophe('Custom texts')}}" stepKey="grabContentFromWidget"/> + <assertEquals message="Asserts the widget contains apostrophe On storefront" stepKey="assertApostropheOnWidgetText"> + <expectedResult type="string">Custom texts' for tests</expectedResult> + <actualResult type="string">{$grabContentFromWidget}</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php index 3a2d9061e7f36..f194bab868ec9 100644 --- a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php +++ b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php @@ -97,7 +97,7 @@ public function execute() $secretKey = $this->getRequest()->getParam('key'); if ($secretKey == $info['secret_key']) { - $this->_fileResponseFactory->create( + return $this->_fileResponseFactory->create( $info['title'], ['value' => $info['quote_path'], 'type' => 'filename'], DirectoryList::MEDIA, diff --git a/app/code/Magento/Wishlist/README.md b/app/code/Magento/Wishlist/README.md index a0ec7d70d9955..3c1d0af7390c5 100644 --- a/app/code/Magento/Wishlist/README.md +++ b/app/code/Magento/Wishlist/README.md @@ -85,12 +85,13 @@ This module introduces the following layouts and layout handles in the directori - `wishlist_index_index` - `wishlist_index_share` - `wishlist_shared_index.xml` - + For more information about a layout, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components -You can extend a customer form and widgets using the configuration files located in the directories +You can extend a customer form and widgets using the configuration files located in the directories + - `view/base/ui_component`: - `customer_form` - `view/frontend/ui_component`: diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertOnlyOneProductPresentedInWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertOnlyOneProductPresentedInWishlistActionGroup.xml new file mode 100644 index 0000000000000..1bf48905fa59e --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertOnlyOneProductPresentedInWishlistActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertOnlyOneProductPresentedInWishlistActionGroup"> + <annotations> + <description>Assert that there is only one product with specific product name and price on the Wishlist on hover.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + <argument name="productPrice" type="string"/> + </arguments> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="goToWishList"/> + <waitForPageLoad stepKey="waitForWishList"/> + <waitForElement selector="{{StorefrontCustomerWishlistProductSection.ProductTitleByName(productName)}}" time="30" stepKey="assertProductName"/> + <see userInput="{{productPrice}}" selector="{{StorefrontCustomerWishlistProductSection.ProductPriceByName(productName)}}" stepKey="assertProductPrice"/> + + <grabMultiple selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName(productName)}}" stepKey="countProductsInWishlist"/> + <assertCount stepKey="check"> + <expectedResult type="int">1</expectedResult> + <actualResult type="variable">countProductsInWishlist</actualResult> + </assertCount> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminConfigureCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminConfigureCustomerWishListItemTest.xml index 6c4ddf2bfd006..2d1294f9e0391 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminConfigureCustomerWishListItemTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminConfigureCustomerWishListItemTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-40455"/> <useCaseId value="MC-37418"/> <group value="wishlist"/> + <group value="cloud"/> </annotations> <before> <!-- Create the configurable product based on the data in the /data folder --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml index b1cb8fdd22811..e4ecb7ccc4f56 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-35170"/> <group value="wishlist"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml index f44b8158f1f79..cd66f76679431 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MAGETWO-95897"/> <useCaseId value="MAGETWO-95837"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithCustomizableFileOptionToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithCustomizableFileOptionToWishlistTest.xml index 1c822dcdcba4d..2cd9264d8ef6f 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithCustomizableFileOptionToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithCustomizableFileOptionToWishlistTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-40417"/> <group value="wishlist"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithTextFieldAndAreaAndFileOptionsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithTextFieldAndAreaAndFileOptionsToWishlistTest.xml new file mode 100644 index 0000000000000..7c990cdc476ea --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddSimpleProductWithTextFieldAndAreaAndFileOptionsToWishlistTest.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="StorefrontAddSimpleProductWithTextFieldAndAreaAndFileOptionsToWishlistTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Add product to wishlist"/> + <title value="Add simple product with customizable file and text and area options to wishlist"/> + <description value="Add simple Product with customizable file and text and area options to Wishlist and verify customizable options are preserved, and the product was added only once"/> + <severity value="AVERAGE"/> + <testCaseId value="https://github.com/magento/magento2/issues/37437"/> + <useCaseId value="https://github.com/magento/magento2/issues/37437"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">100.00</field> + </createData> + <updateData entity="ProductWithTextFieldAndAreaAndFileOptions" createDataKey="createProduct" stepKey="updateProductWithOptions"> + <requiredEntity createDataKey="createProduct"/> + </updateData> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct1"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <!-- Open Product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <attachFile userInput="adobe-base.jpg" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> + + <!-- Add product to the wishlist --> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductWithOptionToWishlist"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + + <!-- Assert product is present in wishlist --> + <actionGroup ref="AssertOnlyOneProductPresentedInWishlistActionGroup" stepKey="assertProductPresent"> + <argument name="productName" value="$createProduct.name$"/> + <argument name="productPrice" value="$129.99"/> + </actionGroup> + + <!-- Edit wishlist product --> + <actionGroup ref="StorefrontCustomerUpdateWishlistItemActionGroup" stepKey="clickEditWishlistItem"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <!-- Update product in wishlist from product page --> + <actionGroup ref="StorefrontCustomerUpdateProductInWishlistActionGroup" stepKey="updateProductWithOptionInWishlist"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + + <actionGroup ref="AssertOnlyOneProductPresentedInWishlistActionGroup" stepKey="assertProductPresent2"> + <argument name="productName" value="$createProduct.name$"/> + <argument name="productPrice" value="$129.99"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml index 0df5ae6416321..2741cc1b727ed 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14211"/> <group value="wishlist"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml index 6b850cd1d24fa..1ebbdd68f8f6e 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14216"/> <group value="wishlist"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml index 1e423643ee66d..dd9a8a3ee2a16 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14217"/> <group value="wishlist"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml index cad5eb0e748d1..d593e9a29f674 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14211"/> <group value="wishlist"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml index 7bbfedc58c818..7faf4b6a8551b 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14213"/> <group value="wishlist"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateConfigurableProductAttributeOptionFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateConfigurableProductAttributeOptionFromWishlistTest.xml index 5d53f58afc3a7..25ecc82959c94 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateConfigurableProductAttributeOptionFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateConfigurableProductAttributeOptionFromWishlistTest.xml @@ -20,6 +20,7 @@ <group value="catalog"/> <group value="configurableProduct"/> <group value="wishlist"/> + <group value="cloud"/> </annotations> <before> <!-- Create Customer --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 86d09783e0f55..ea7f3aa0d9e2d 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94296"/> <group value="Wishlist"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml index 96e5417f1cffd..3c11d8f92c4b3 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-99228"/> <testCaseId value="MC-16050"/> <group value="wishlist"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> 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 45c12d1c3ae3f..7ccf41d3d57bb 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 @@ -225,12 +225,22 @@ define([ }); }, + /** + * Unbind previous form submit listener. + */ + unbindFormSubmit: function () { + $('[data-action="add-to-wishlist"]').off('click'); + }, + /** * Bind form submit. */ bindFormSubmit: function () { var self = this; + // Prevents double handlers and duplicate requests to add to Wishlist + this.unbindFormSubmit(); + $('[data-action="add-to-wishlist"]').on('click', function (event) { var element, params, form, action; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 3394e8a4b50cf..41eca2db2fce2 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -70,6 +70,7 @@ .not-calculated { font-style: italic; + white-space: normal; } // @@ -82,15 +83,7 @@ border-bottom: @border-width__base solid @border-color__base; .lib-css(padding, @indent__s @indent__xl @indent__s 0); cursor: pointer; - .lib-icon-font( - @icon-down, - @_icon-font-size: 30px, - @_icon-font-line-height: 12px, - @_icon-font-text-hide: true, - @_icon-font-margin: 3px 0 0, - @_icon-font-position: after, - @_icon-font-display: block - ); + .lib-icon-font(@icon-down, @_icon-font-size: 30px, @_icon-font-line-height: 12px, @_icon-font-text-hide: true, @_icon-font-margin: 3px 0 0, @_icon-font-position: after, @_icon-font-display: block); margin-bottom: 0; position: relative; @@ -109,10 +102,7 @@ &.active { > .title { - .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after - ); + .lib-icon-font-symbol(@_icon-font-content: @icon-up, @_icon-font-position: after); } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 920e68994c666..6ae6a23b9b057 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -70,6 +70,7 @@ .not-calculated { font-style: italic; + white-space: normal; } // @@ -82,15 +83,7 @@ border-bottom: @border-width__base solid @border-color__base; .lib-css(padding, @indent__s @indent__xl @indent__s 0); cursor: pointer; - .lib-icon-font( - @icon-down, - @_icon-font-size: 12px, - @_icon-font-line-height: 12px, - @_icon-font-text-hide: true, - @_icon-font-margin: 3px 0 0, - @_icon-font-position: after, - @_icon-font-display: block - ); + .lib-icon-font(@icon-down, @_icon-font-size: 12px, @_icon-font-line-height: 12px, @_icon-font-text-hide: true, @_icon-font-margin: 3px 0 0, @_icon-font-position: after, @_icon-font-display: block); margin-bottom: 0; position: relative; @@ -109,10 +102,7 @@ &.active { > .title { - .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after - ); + .lib-icon-font-symbol(@_icon-font-content: @icon-up, @_icon-font-position: after); } } diff --git a/app/etc/di.xml b/app/etc/di.xml index 099dd84d83e76..12017a4e94429 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -212,6 +212,7 @@ <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <preference for="Magento\Framework\Interception\ConfigLoaderInterface" type="Magento\Framework\Interception\PluginListGenerator" /> <preference for="Magento\Framework\Interception\ConfigWriterInterface" type="Magento\Framework\Interception\PluginListGenerator" /> + <preference for="Magento\Framework\Mview\View\SubscriptionStatementPostprocessorInterface" type="Magento\Framework\Mview\View\CompositeSubscriptionStatementPostprocessor" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> @@ -374,6 +375,11 @@ <argument name="resource" xsi:type="object">Magento\Framework\App\ResourceConnection\Proxy</argument> </arguments> </type> + <type name="Magento\Framework\Cache\InvalidateLogger"> + <arguments> + <argument name="request" xsi:type="object">Magento\Framework\App\Request\Http\Proxy</argument> + </arguments> + </type> <type name="Magento\Backend\App\Area\FrontNameResolver"> <arguments> <argument name="defaultFrontName" xsi:type="init_parameter">Magento\Backend\Setup\ConfigOptionsList::CONFIG_PATH_BACKEND_FRONTNAME</argument> @@ -1814,11 +1820,6 @@ </argument> </arguments> </type> - <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> - <arguments> - <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Database</argument> - </arguments> - </type> <type name="Magento\Framework\Cache\CompositeStaleCacheNotifier"> <arguments> <argument name="notifiers" xsi:type="array"> @@ -2002,4 +2003,5 @@ </arguments> </type> <preference for="Magento\Framework\Filter\Input\PurifierInterface" type="Magento\Framework\Filter\Input\Purifier"/> + <preference for="Magento\Framework\App\PageCache\IdentifierInterface" type="Magento\Framework\App\PageCache\Identifier"/> </config> diff --git a/composer.json b/composer.json index 0c01c49b1ec77..cf3f8c04a394b 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,9 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "magento/*": true + "laminas/laminas-dependency-plugin": true, + "magento/*": true, + "php-http/discovery": true }, "preferred-install": "dist", "sort-packages": true @@ -38,9 +40,10 @@ "colinmollenhour/credis": "^1.13", "colinmollenhour/php-redis-session-abstract": "^1.5", "composer/composer": "^2.0, !=2.2.16", - "elasticsearch/elasticsearch": "^7.17||^8.5", - "ezyang/htmlpurifier": "^4.14", - "guzzlehttp/guzzle": "^7.4", + "doctrine/annotations": "^1.13", + "elasticsearch/elasticsearch": "~7.17.0 || ~8.5.0", + "ezyang/htmlpurifier": "^4.16", + "guzzlehttp/guzzle": "^7.5", "laminas/laminas-captcha": "^2.12", "laminas/laminas-code": "^4.5", "laminas/laminas-db": "^2.15", @@ -65,15 +68,15 @@ "laminas/laminas-validator": "^2.23", "league/flysystem": "^2.4", "league/flysystem-aws-s3-v3": "^2.4", - "magento/composer": "^1.9.0-beta1", + "magento/composer": "^1.9.0", "magento/composer-dependency-version-audit-plugin": "^0.1", - "magento/magento-composer-installer": ">=0.4.0-beta1", + "magento/magento-composer-installer": ">=0.4.0", "magento/zend-cache": "^1.16", "magento/zend-db": "^1.16", "magento/zend-pdf": "^1.16", "monolog/monolog": "^2.7", - "opensearch-project/opensearch-php": "^1.0 || ^2.0", - "pelago/emogrifier": "^6.0.0", + "opensearch-project/opensearch-php": "^1.0 || ^2.0, <2.0.1", + "pelago/emogrifier": "^7.0", "php-amqplib/php-amqplib": "^3.2", "phpseclib/mcrypt_compat": "^2.0", "phpseclib/phpseclib": "^3.0", @@ -85,8 +88,8 @@ "tedivm/jshrink": "^1.4", "tubalmartin/cssmin": "^4.1", "web-token/jwt-framework": "^3.1", - "webonyx/graphql-php": "^14.11", - "wikimedia/less.php": "^3.0" + "webonyx/graphql-php": "^15.0", + "wikimedia/less.php": "^3.2" }, "require-dev": { "allure-framework/allure-phpunit": "^2", @@ -95,13 +98,14 @@ "friendsofphp/php-cs-fixer": "^3.8", "lusitanian/oauth": "^0.8", "magento/magento-coding-standard": "*", - "magento/magento2-functional-testing-framework": "^4.0", + "magento/magento2-functional-testing-framework": "^4.3.0", "pdepend/pdepend": "^2.10", "phpmd/phpmd": "^2.12", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "<=9.5.22", "sebastian/phpcpd": "^6.0", - "symfony/finder": "^5.4" + "symfony/finder": "^5.4", + "sebastian/comparator": "<=4.0.6" }, "suggest": { "ext-pcntl": "Need for run processes in parallel mode" @@ -148,6 +152,7 @@ "magento/module-configurable-product": "*", "magento/module-configurable-product-sales": "*", "magento/module-contact": "*", + "magento/module-contact-graph-ql": "*", "magento/module-cookie": "*", "magento/module-cron": "*", "magento/module-currency-symbol": "*", @@ -167,7 +172,6 @@ "magento/module-open-search": "*", "magento/module-elasticsearch": "*", "magento/module-elasticsearch-7": "*", - "magento/module-elasticsearch-8": "*", "magento/module-email": "*", "magento/module-encryption-key": "*", "magento/module-fedex": "*", @@ -179,6 +183,7 @@ "magento/module-google-gtag": "*", "magento/module-graph-ql": "*", "magento/module-graph-ql-cache": "*", + "magento/module-graph-ql-resolver-cache": "*", "magento/module-catalog-graph-ql": "*", "magento/module-catalog-cms-graph-ql": "*", "magento/module-catalog-url-rewrite-graph-ql": "*", diff --git a/composer.lock b/composer.lock index f0cd4e630a62d..081de9c4b52a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,31 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b8146107f5215900d0c6a26fb71a630a", + "content-hash": "df2bfe94e9160251d0a853ee3531942a", "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.0.2", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "3942776a8c99209908ee0b287746263725685732" + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/3942776a8c99209908ee0b287746263725685732", - "reference": "3942776a8c99209908ee0b287746263725685732", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5", + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5", "shasum": "" }, "require": { "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.8.35|^5.4.3" + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." }, "type": "library", "autoload": { @@ -43,7 +47,7 @@ } ], "description": "AWS Common Runtime for PHP", - "homepage": "http://aws.amazon.com/sdkforphp", + "homepage": "https://github.com/awslabs/aws-crt-php", "keywords": [ "amazon", "aws", @@ -52,39 +56,42 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.2" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1" }, - "time": "2021-09-03T22:57:30+00:00" + "time": "2023-03-24T20:22:19+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.224.4", + "version": "3.275.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8c8a713b8c1e1a20f66a801f9d2cd7fd80d8d3f8" + "reference": "6cf6aacecda1dec52bf4a70d8e1503b5bc56e924" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8c8a713b8c1e1a20f66a801f9d2cd7fd80d8d3f8", - "reference": "8c8a713b8c1e1a20f66a801f9d2cd7fd80d8d3f8", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6cf6aacecda1dec52bf4a70d8e1503b5bc56e924", + "reference": "6cf6aacecda1dec52bf4a70d8e1503b5bc56e924", "shasum": "" }, "require": { - "aws/aws-crt-php": "^1.0.2", + "aws/aws-crt-php": "^1.0.4", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", "guzzlehttp/promises": "^1.4.0", - "guzzlehttp/psr7": "^1.7.0 || ^2.1.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "mtdowling/jmespath.php": "^2.6", - "php": ">=5.5" + "php": ">=5.5", + "psr/http-message": "^1.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", @@ -92,10 +99,11 @@ "ext-sockets": "*", "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35 || ^5.6.3", + "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -143,32 +151,32 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.224.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.275.1" }, - "time": "2022-06-06T18:32:10+00:00" + "time": "2023-06-30T18:23:40+00:00" }, { "name": "brick/math", - "version": "0.9.3", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" + "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", + "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f", + "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f", "shasum": "" }, "require": { "ext-json": "*", - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", - "vimeo/psalm": "4.9.2" + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "4.25.0" }, "type": "library", "autoload": { @@ -193,19 +201,15 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.9.3" + "source": "https://github.com/brick/math/tree/0.10.2" }, "funding": [ { "url": "https://github.com/BenMorel", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" } ], - "time": "2021-08-15T20:50:18+00:00" + "time": "2022-08-10T22:54:19+00:00" }, { "name": "brick/varexporter", @@ -329,16 +333,16 @@ }, { "name": "colinmollenhour/credis", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/colinmollenhour/credis.git", - "reference": "afec8e58ec93d2291c127fa19709a048f28641e5" + "reference": "dccc8a46586475075fbb012d8bd523b8a938c2dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/afec8e58ec93d2291c127fa19709a048f28641e5", - "reference": "afec8e58ec93d2291c127fa19709a048f28641e5", + "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/dccc8a46586475075fbb012d8bd523b8a938c2dc", + "reference": "dccc8a46586475075fbb012d8bd523b8a938c2dc", "shasum": "" }, "require": { @@ -370,9 +374,9 @@ "homepage": "https://github.com/colinmollenhour/credis", "support": { "issues": "https://github.com/colinmollenhour/credis/issues", - "source": "https://github.com/colinmollenhour/credis/tree/v1.13.0" + "source": "https://github.com/colinmollenhour/credis/tree/v1.14.0" }, - "time": "2022-04-07T14:57:22+00:00" + "time": "2022-11-09T01:18:39+00:00" }, { "name": "colinmollenhour/php-redis-session-abstract", @@ -420,16 +424,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.3.4", + "version": "1.3.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5" + "reference": "90d087e988ff194065333d16bc5cf649872d9cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/69098eca243998b53eed7a48d82dedd28b447cd5", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/90d087e988ff194065333d16bc5cf649872d9cdb", + "reference": "90d087e988ff194065333d16bc5cf649872d9cdb", "shasum": "" }, "require": { @@ -476,7 +480,80 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.4" + "source": "https://github.com/composer/ca-bundle/tree/1.3.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-06-06T12:02:59+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.1.0" }, "funding": [ { @@ -492,43 +569,52 @@ "type": "tidelift" } ], - "time": "2022-10-12T12:08:29+00:00" + "time": "2023-06-30T13:58:57+00:00" }, { "name": "composer/composer", - "version": "2.2.18", + "version": "2.5.8", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "84175907664ca8b73f73f4883e67e886dfefb9f5" + "reference": "4c516146167d1392c8b9b269bb7c24115d262164" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/84175907664ca8b73f73f4883e67e886dfefb9f5", - "reference": "84175907664ca8b73f73f4883e67e886dfefb9f5", + "url": "https://api.github.com/repos/composer/composer/zipball/4c516146167d1392c8b9b269bb7c24115d262164", + "reference": "4c516146167d1392c8b9b269bb7c24115d262164", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0", + "composer/class-map-generator": "^1.0", "composer/metadata-minifier": "^1.0", - "composer/pcre": "^1.0", + "composer/pcre": "^2.1 || ^3.1", "composer/semver": "^3.0", - "composer/spdx-licenses": "^1.2", - "composer/xdebug-handler": "^2.0 || ^3.0", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", "justinrainbow/json-schema": "^5.2.11", - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0 || ^2.0", - "react/promise": "^1.2 || ^2.7", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.8", "seld/jsonlint": "^1.4", - "seld/phar-utils": "^1.0", - "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", - "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.11 || ^6.0.11", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4 || ^6.0" }, "require-dev": { - "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "phpstan/phpstan": "^1.9.3", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1", + "phpstan/phpstan-symfony": "^1.2.10", + "symfony/phpunit-bridge": "^6.0" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -541,7 +627,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.2-dev" + "dev-main": "2.5-dev" + }, + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] } }, "autoload": { @@ -575,7 +666,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.2.18" + "source": "https://github.com/composer/composer/tree/2.5.8" }, "funding": [ { @@ -591,7 +682,7 @@ "type": "tidelift" } ], - "time": "2022-08-20T09:33:38+00:00" + "time": "2023-06-09T15:13:21+00:00" }, { "name": "composer/metadata-minifier", @@ -664,30 +755,30 @@ }, { "name": "composer/pcre", - "version": "1.0.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -715,7 +806,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.1" + "source": "https://github.com/composer/pcre/tree/3.1.0" }, "funding": [ { @@ -731,7 +822,7 @@ "type": "tidelift" } ], - "time": "2022-01-21T20:24:37+00:00" + "time": "2022-11-17T09:50:14+00:00" }, { "name": "composer/semver", @@ -960,6 +1051,158 @@ ], "time": "2022-02-25T21:32:43+00:00" }, + { + "name": "doctrine/annotations", + "version": "1.14.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1 || ^2", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "~1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.14.3" + }, + "time": "2023-02-01T09:20:38+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, { "name": "elasticsearch/elasticsearch", "version": "v7.17.1", @@ -1078,16 +1321,16 @@ }, { "name": "ezimuel/ringphp", - "version": "1.2.0", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/ezimuel/ringphp.git", - "reference": "92b8161404ab1ad84059ebed41d9f757e897ce74" + "reference": "7887fc8488013065f72f977dcb281994f5fde9f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezimuel/ringphp/zipball/92b8161404ab1ad84059ebed41d9f757e897ce74", - "reference": "92b8161404ab1ad84059ebed41d9f757e897ce74", + "url": "https://api.github.com/repos/ezimuel/ringphp/zipball/7887fc8488013065f72f977dcb281994f5fde9f4", + "reference": "7887fc8488013065f72f977dcb281994f5fde9f4", "shasum": "" }, "require": { @@ -1129,26 +1372,36 @@ ], "description": "Fork of guzzle/RingPHP (abandoned) to be used with elasticsearch-php", "support": { - "source": "https://github.com/ezimuel/ringphp/tree/1.2.0" + "source": "https://github.com/ezimuel/ringphp/tree/1.2.2" }, - "time": "2021-11-16T11:51:30+00:00" + "time": "2022-12-07T11:28:53+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.14.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { @@ -1180,30 +1433,30 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" }, - "time": "2021-12-25T01:21:49+00:00" + "time": "2022-09-18T07:06:19+00:00" }, { "name": "fgrosse/phpasn1", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/fgrosse/PHPASN1.git", - "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296" + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296", - "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", "shasum": "" }, "require": { - "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0" + "php": "^7.1 || ^8.0" }, "require-dev": { "php-coveralls/php-coveralls": "~2.0", - "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "suggest": { "ext-bcmath": "BCmath is the fallback extension for big integer calculations", @@ -1255,28 +1508,29 @@ ], "support": { "issues": "https://github.com/fgrosse/PHPASN1/issues", - "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0" + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" }, - "time": "2021-12-11T12:41:06+00:00" + "abandoned": true, + "time": "2022-12-19T11:08:26+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.4.3", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab" + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/74a8602c6faec9ef74b7a9391ac82c5e65b1cdab", - "reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1285,10 +1539,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1298,8 +1553,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -1365,7 +1621,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.4.3" + "source": "https://github.com/guzzle/guzzle/tree/7.7.0" }, "funding": [ { @@ -1381,20 +1637,20 @@ "type": "tidelift" } ], - "time": "2022-05-25T13:24:33+00:00" + "time": "2023-05-21T14:04:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", "shasum": "" }, "require": { @@ -1404,11 +1660,6 @@ "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, "autoload": { "files": [ "src/functions_include.php" @@ -1449,7 +1700,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/1.5.3" }, "funding": [ { @@ -1465,26 +1716,26 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2023-05-21T12:31:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.2.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + "reference": "b635f279edd83fc275f822a1188157ffea568ff6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0" }, "provide": { @@ -1492,17 +1743,18 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.1", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.8 || ^9.3.10" + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.2-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -1564,7 +1816,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.2.1" + "source": "https://github.com/guzzle/psr7/tree/2.5.0" }, "funding": [ { @@ -1580,7 +1832,7 @@ "type": "tidelift" } ], - "time": "2022-03-20T21:55:58+00:00" + "time": "2023-04-17T16:11:26+00:00" }, { "name": "justinrainbow/json-schema", @@ -1654,35 +1906,35 @@ }, { "name": "laminas/laminas-captcha", - "version": "2.12.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-captcha.git", - "reference": "b07e499a7df73795768aa89e0138757a7ddb9195" + "reference": "de816814f52c67b33db614deb6227d46df531bc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-captcha/zipball/b07e499a7df73795768aa89e0138757a7ddb9195", - "reference": "b07e499a7df73795768aa89e0138757a7ddb9195", + "url": "https://api.github.com/repos/laminas/laminas-captcha/zipball/de816814f52c67b33db614deb6227d46df531bc6", + "reference": "de816814f52c67b33db614deb6227d46df531bc6", "shasum": "" }, "require": { - "laminas/laminas-math": "^2.7 || ^3.0", - "laminas/laminas-recaptcha": "^3.0", + "laminas/laminas-recaptcha": "^3.4.0", "laminas/laminas-session": "^2.12", - "laminas/laminas-stdlib": "^3.6", - "laminas/laminas-text": "^2.8", - "laminas/laminas-validator": "^2.14", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-stdlib": "^3.10.1", + "laminas/laminas-text": "^2.9.0", + "laminas/laminas-validator": "^2.19.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-captcha": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.1.4", - "phpunit/phpunit": "^9.4.3", - "psalm/plugin-phpunit": "^0.15.1", - "vimeo/psalm": "^4.6" + "ext-gd": "*", + "laminas/laminas-coding-standard": "~2.4.0", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^4.29.0" }, "suggest": { "laminas/laminas-i18n-resources": "Translations of captcha messages" @@ -1717,33 +1969,33 @@ "type": "community_bridge" } ], - "time": "2022-04-07T10:41:09+00:00" + "time": "2022-11-15T23:25:43+00:00" }, { "name": "laminas/laminas-code", - "version": "4.5.2", + "version": "4.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-code.git", - "reference": "da01fb74c08f37e20e7ae49f1e3ee09aa401ebad" + "reference": "dd19fe8e07cc3f374308565667eecd4958c22106" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/da01fb74c08f37e20e7ae49f1e3ee09aa401ebad", - "reference": "da01fb74c08f37e20e7ae49f1e3ee09aa401ebad", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/dd19fe8e07cc3f374308565667eecd4958c22106", + "reference": "dd19fe8e07cc3f374308565667eecd4958c22106", "shasum": "" }, "require": { - "php": ">=7.4, <8.2" + "php": "~8.1.0 || ~8.2.0" }, "require-dev": { - "doctrine/annotations": "^1.13.2", + "doctrine/annotations": "^1.13.3", "ext-phar": "*", "laminas/laminas-coding-standard": "^2.3.0", "laminas/laminas-stdlib": "^3.6.1", - "phpunit/phpunit": "^9.5.10", - "psalm/plugin-phpunit": "^0.16.1", - "vimeo/psalm": "^4.13.1" + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.1.0" }, "suggest": { "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", @@ -1751,9 +2003,6 @@ }, "type": "library", "autoload": { - "files": [ - "polyfill/ReflectionEnumPolyfill.php" - ], "psr-4": { "Laminas\\Code\\": "src/" } @@ -1783,26 +2032,26 @@ "type": "community_bridge" } ], - "time": "2022-06-06T11:26:02+00:00" + "time": "2022-12-08T02:08:23+00:00" }, { "name": "laminas/laminas-config", - "version": "3.7.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-config.git", - "reference": "e43d13dcfc273d4392812eb395ce636f73f34dfd" + "reference": "46baad58d0b12cf98539e04334eff40a1fdfb9a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-config/zipball/e43d13dcfc273d4392812eb395ce636f73f34dfd", - "reference": "e43d13dcfc273d4392812eb395ce636f73f34dfd", + "url": "https://api.github.com/repos/laminas/laminas-config/zipball/46baad58d0b12cf98539e04334eff40a1fdfb9a0", + "reference": "46baad58d0b12cf98539e04334eff40a1fdfb9a0", "shasum": "" }, "require": { "ext-json": "*", "laminas/laminas-stdlib": "^3.6", - "php": "^7.3 || ~8.0.0 || ~8.1.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "psr/container": "^1.0" }, "conflict": { @@ -1810,11 +2059,11 @@ "zendframework/zend-config": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", - "laminas/laminas-filter": "^2.7.2", - "laminas/laminas-i18n": "^2.10.3", - "laminas/laminas-servicemanager": "^3.7", - "phpunit/phpunit": "^9.5.5" + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-filter": "~2.23.0", + "laminas/laminas-i18n": "~2.19.0", + "laminas/laminas-servicemanager": "~3.19.0", + "phpunit/phpunit": "~9.5.25" }, "suggest": { "laminas/laminas-filter": "^2.7.2; install if you want to use the Filter processor", @@ -1851,20 +2100,20 @@ "type": "community_bridge" } ], - "time": "2021-10-01T16:07:46+00:00" + "time": "2022-10-16T14:21:22+00:00" }, { "name": "laminas/laminas-crypt", - "version": "3.8.0", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-crypt.git", - "reference": "0972bb907fd555c16e2a65309b66720acf2b8699" + "reference": "56ab1b195dad5456753601ff2e8e3d3fd9392d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-crypt/zipball/0972bb907fd555c16e2a65309b66720acf2b8699", - "reference": "0972bb907fd555c16e2a65309b66720acf2b8699", + "url": "https://api.github.com/repos/laminas/laminas-crypt/zipball/56ab1b195dad5456753601ff2e8e3d3fd9392d1a", + "reference": "56ab1b195dad5456753601ff2e8e3d3fd9392d1a", "shasum": "" }, "require": { @@ -1872,15 +2121,15 @@ "laminas/laminas-math": "^3.4", "laminas/laminas-servicemanager": "^3.11.2", "laminas/laminas-stdlib": "^3.6", - "php": "^7.4 || ~8.0.0 || ~8.1.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "psr/container": "^1.1" }, "conflict": { "zendframework/zend-crypt": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.3.0", - "phpunit/phpunit": "^9.5.11" + "laminas/laminas-coding-standard": "~2.4.0", + "phpunit/phpunit": "^9.5.25" }, "suggest": { "ext-openssl": "Required for most features of Laminas\\Crypt" @@ -1915,35 +2164,35 @@ "type": "community_bridge" } ], - "time": "2022-04-12T14:28:29+00:00" + "time": "2022-10-16T15:51:01+00:00" }, { "name": "laminas/laminas-db", - "version": "2.15.0", + "version": "2.16.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-db.git", - "reference": "1125ef2e55108bdfcc1f0030d3a0f9b895e09606" + "reference": "dadd9a19d2f9e89aa59205572b928892b91ff1da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-db/zipball/1125ef2e55108bdfcc1f0030d3a0f9b895e09606", - "reference": "1125ef2e55108bdfcc1f0030d3a0f9b895e09606", + "url": "https://api.github.com/repos/laminas/laminas-db/zipball/dadd9a19d2f9e89aa59205572b928892b91ff1da", + "reference": "dadd9a19d2f9e89aa59205572b928892b91ff1da", "shasum": "" }, "require": { "laminas/laminas-stdlib": "^3.7.1", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "php": "~8.0.0 || ~8.1.0|| ~8.2.0" }, "conflict": { "zendframework/zend-db": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.2.1", - "laminas/laminas-eventmanager": "^3.4.0", - "laminas/laminas-hydrator": "^3.2 || ^4.3", - "laminas/laminas-servicemanager": "^3.7.0", - "phpunit/phpunit": "^9.5.19" + "laminas/laminas-coding-standard": "^2.4.0", + "laminas/laminas-eventmanager": "^3.6.0", + "laminas/laminas-hydrator": "^4.7", + "laminas/laminas-servicemanager": "^3.19.0", + "phpunit/phpunit": "^9.5.25" }, "suggest": { "laminas/laminas-eventmanager": "Laminas\\EventManager component", @@ -1986,42 +2235,42 @@ "type": "community_bridge" } ], - "time": "2022-04-11T13:26:20+00:00" + "time": "2022-12-17T16:31:58+00:00" }, { "name": "laminas/laminas-di", - "version": "3.7.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-di.git", - "reference": "80c90d68bc15d4e094a609760144ce1d1aad0a79" + "reference": "45c9dfd57370617d2028e597061c4ef2a2ea0118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-di/zipball/80c90d68bc15d4e094a609760144ce1d1aad0a79", - "reference": "80c90d68bc15d4e094a609760144ce1d1aad0a79", + "url": "https://api.github.com/repos/laminas/laminas-di/zipball/45c9dfd57370617d2028e597061c4ef2a2ea0118", + "reference": "45c9dfd57370617d2028e597061c4ef2a2ea0118", "shasum": "" }, "require": { "laminas/laminas-stdlib": "^3.6", - "php": ">=7.4, <8.2", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "psr/container": "^1.1.1", - "psr/log": "^1.1.4" + "psr/log": "^1.1.4 || ^3.0.0" }, "conflict": { + "laminas/laminas-servicemanager": "<3.13.0", "laminas/laminas-servicemanager-di": "*", - "phpspec/prophecy": "<1.9.0", "zendframework/zend-di": "*" }, "require-dev": { - "container-interop/container-interop": "^1.2.0", - "laminas/laminas-coding-standard": "~2.3.0", - "laminas/laminas-servicemanager": "^3.7", - "mikey179/vfsstream": "^1.6.10@alpha", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^0.12.64", - "phpunit/phpunit": "^9.5.5", - "squizlabs/php_codesniffer": "^3.6" + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-servicemanager": "^3.12", + "mikey179/vfsstream": "^1.6.11@alpha", + "phpbench/phpbench": "^1.2.7", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.16.1", + "squizlabs/php_codesniffer": "^3.7.1", + "vimeo/psalm": "^4.10" }, "suggest": { "laminas/laminas-servicemanager": "An IoC container without auto wiring capabilities" @@ -2063,7 +2312,7 @@ "type": "community_bridge" } ], - "time": "2022-05-15T18:19:36+00:00" + "time": "2022-11-25T10:24:48+00:00" }, { "name": "laminas/laminas-escaper", @@ -2129,16 +2378,16 @@ }, { "name": "laminas/laminas-eventmanager", - "version": "3.6.0", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-eventmanager.git", - "reference": "3f1afbad86cd34a431fdc069f265cfe6f8fc8308" + "reference": "74c091fb0da37744e7d215ef5bd3564c77f6385e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/3f1afbad86cd34a431fdc069f265cfe6f8fc8308", - "reference": "3f1afbad86cd34a431fdc069f265cfe6f8fc8308", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/74c091fb0da37744e7d215ef5bd3564c77f6385e", + "reference": "74c091fb0da37744e7d215ef5bd3564c77f6385e", "shasum": "" }, "require": { @@ -2151,11 +2400,11 @@ "require-dev": { "laminas/laminas-coding-standard": "~2.4.0", "laminas/laminas-stdlib": "^3.15", - "phpbench/phpbench": "^1.2.6", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", + "phpbench/phpbench": "^1.2.7", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", "psr/container": "^1.1.2 || ^2.0.2", - "vimeo/psalm": "^4.28" + "vimeo/psalm": "^5.0.0" }, "suggest": { "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature", @@ -2193,20 +2442,20 @@ "type": "community_bridge" } ], - "time": "2022-10-11T12:46:13+00:00" + "time": "2022-12-10T16:36:52+00:00" }, { "name": "laminas/laminas-feed", - "version": "2.19.0", + "version": "2.20.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-feed.git", - "reference": "4d0a7a536b48f698914156ca6633104b3aef2f3b" + "reference": "508ebef6e622f2f2ce3dd0559739ffd0dfa3b938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/4d0a7a536b48f698914156ca6633104b3aef2f3b", - "reference": "4d0a7a536b48f698914156ca6633104b3aef2f3b", + "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/508ebef6e622f2f2ce3dd0559739ffd0dfa3b938", + "reference": "508ebef6e622f2f2ce3dd0559739ffd0dfa3b938", "shasum": "" }, "require": { @@ -2226,12 +2475,12 @@ "laminas/laminas-cache-storage-adapter-memory": "^1.1.0 || ^2.1", "laminas/laminas-coding-standard": "~2.4.0", "laminas/laminas-db": "^2.15", - "laminas/laminas-http": "^2.16", + "laminas/laminas-http": "^2.17.0", "laminas/laminas-validator": "^2.26", "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", + "psalm/plugin-phpunit": "^0.18.0", "psr/http-message": "^1.0.1", - "vimeo/psalm": "^4.29" + "vimeo/psalm": "^5.1.0" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component, for optionally caching feeds between requests", @@ -2273,25 +2522,25 @@ "type": "community_bridge" } ], - "time": "2022-10-14T13:40:45+00:00" + "time": "2022-12-03T19:40:30+00:00" }, { "name": "laminas/laminas-file", - "version": "2.11.0", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-file.git", - "reference": "8eebc51715188032161fbafeae22a618af16bdb3" + "reference": "9e8ff3a6d7ccaad0865581ef672a7c48260b65d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-file/zipball/8eebc51715188032161fbafeae22a618af16bdb3", - "reference": "8eebc51715188032161fbafeae22a618af16bdb3", + "url": "https://api.github.com/repos/laminas/laminas-file/zipball/9e8ff3a6d7ccaad0865581ef672a7c48260b65d9", + "reference": "9e8ff3a6d7ccaad0865581ef672a7c48260b65d9", "shasum": "" }, "require": { - "laminas/laminas-stdlib": "^2.7.7 || ^3.1", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-stdlib": "^2.7.7 || ^3.15.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-file": "*" @@ -2341,20 +2590,20 @@ "type": "community_bridge" } ], - "time": "2022-02-15T07:34:03+00:00" + "time": "2022-11-21T06:59:25+00:00" }, { "name": "laminas/laminas-filter", - "version": "2.23.0", + "version": "2.30.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-filter.git", - "reference": "41cff2f850753f0bb3fc75c5ce011fcad6aa1731" + "reference": "97e3ce0fa868567aa433ed34d6f57ee703d70d3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-filter/zipball/41cff2f850753f0bb3fc75c5ce011fcad6aa1731", - "reference": "41cff2f850753f0bb3fc75c5ce011fcad6aa1731", + "url": "https://api.github.com/repos/laminas/laminas-filter/zipball/97e3ce0fa868567aa433ed34d6f57ee703d70d3e", + "reference": "97e3ce0fa868567aa433ed34d6f57ee703d70d3e", "shasum": "" }, "require": { @@ -2369,13 +2618,13 @@ }, "require-dev": { "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-crypt": "^3.8", - "laminas/laminas-uri": "^2.9.1", + "laminas/laminas-crypt": "^3.9", + "laminas/laminas-uri": "^2.10", "pear/archive_tar": "^1.4.14", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", + "phpunit/phpunit": "^9.5.27", + "psalm/plugin-phpunit": "^0.18.4", "psr/http-factory": "^1.0.1", - "vimeo/psalm": "^4.28" + "vimeo/psalm": "^5.3" }, "suggest": { "laminas/laminas-crypt": "Laminas\\Crypt component, for encryption filters", @@ -2419,20 +2668,20 @@ "type": "community_bridge" } ], - "time": "2022-10-11T10:04:14+00:00" + "time": "2022-12-19T17:34:24+00:00" }, { "name": "laminas/laminas-http", - "version": "2.17.0", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "ac4588d698c93b56bb7c0608d9a7537a3f057239" + "reference": "76de9008f889bc7088f85a41d0d2b06c2b59c53d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/ac4588d698c93b56bb7c0608d9a7537a3f057239", - "reference": "ac4588d698c93b56bb7c0608d9a7537a3f057239", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/76de9008f889bc7088f85a41d0d2b06c2b59c53d", + "reference": "76de9008f889bc7088f85a41d0d2b06c2b59c53d", "shasum": "" }, "require": { @@ -2484,20 +2733,20 @@ "type": "community_bridge" } ], - "time": "2022-10-16T15:51:48+00:00" + "time": "2022-11-23T15:45:41+00:00" }, { "name": "laminas/laminas-i18n", - "version": "2.19.0", + "version": "2.21.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-i18n.git", - "reference": "ebabca3a6398fc872127bc69a51bda5afc720d67" + "reference": "fbd2d0373aaced4769cba2bf3d1425d55f68abb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/ebabca3a6398fc872127bc69a51bda5afc720d67", - "reference": "ebabca3a6398fc872127bc69a51bda5afc720d67", + "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/fbd2d0373aaced4769cba2bf3d1425d55f68abb1", + "reference": "fbd2d0373aaced4769cba2bf3d1425d55f68abb1", "shasum": "" }, "require": { @@ -2512,18 +2761,18 @@ "zendframework/zend-i18n": "*" }, "require-dev": { - "laminas/laminas-cache": "^3.6", - "laminas/laminas-cache-storage-adapter-memory": "^2.1", + "laminas/laminas-cache": "^3.8", + "laminas/laminas-cache-storage-adapter-memory": "^2.2.0", "laminas/laminas-cache-storage-deprecated-factory": "^1.0.1", "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-config": "^3.7", - "laminas/laminas-eventmanager": "^3.5.0", - "laminas/laminas-filter": "^2.21", - "laminas/laminas-validator": "^2.25", - "laminas/laminas-view": "^2.23", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.28" + "laminas/laminas-config": "^3.8.0", + "laminas/laminas-eventmanager": "^3.7", + "laminas/laminas-filter": "^2.28.1", + "laminas/laminas-validator": "^2.28", + "laminas/laminas-view": "^2.25", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.0.0" }, "suggest": { "laminas/laminas-cache": "You should install this package to cache the translations", @@ -2570,32 +2819,32 @@ "type": "community_bridge" } ], - "time": "2022-10-10T15:48:56+00:00" + "time": "2022-12-02T17:15:52+00:00" }, { "name": "laminas/laminas-json", - "version": "3.3.0", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-json.git", - "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f" + "reference": "7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-json/zipball/9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", - "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", + "url": "https://api.github.com/repos/laminas/laminas-json/zipball/7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec", + "reference": "7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec", "shasum": "" }, "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-json": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.2.1", + "laminas/laminas-coding-standard": "~2.4.0", "laminas/laminas-stdlib": "^2.7.7 || ^3.1", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5.25" }, "suggest": { "laminas/laminas-json-server": "For implementing JSON-RPC servers", @@ -2631,7 +2880,7 @@ "type": "community_bridge" } ], - "time": "2021-09-02T18:02:31+00:00" + "time": "2022-10-17T04:06:45+00:00" }, { "name": "laminas/laminas-loader", @@ -2691,16 +2940,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.19.0", + "version": "2.21.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "edf3832c05165775589af2fc698b5f9984d4c5f1" + "reference": "451b33522a4e7f17e097e45fceea4752c86a2ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/edf3832c05165775589af2fc698b5f9984d4c5f1", - "reference": "edf3832c05165775589af2fc698b5f9984d4c5f1", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/451b33522a4e7f17e097e45fceea4752c86a2ace", + "reference": "451b33522a4e7f17e097e45fceea4752c86a2ace", "shasum": "" }, "require": { @@ -2716,13 +2965,13 @@ }, "require-dev": { "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-crypt": "^3.8.0", - "laminas/laminas-db": "^2.15.0", - "laminas/laminas-servicemanager": "^3.19", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", + "laminas/laminas-crypt": "^3.9.0", + "laminas/laminas-db": "^2.16", + "laminas/laminas-servicemanager": "^3.20", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.4", "symfony/process": "^6.0.11", - "vimeo/psalm": "^4.29" + "vimeo/psalm": "^5.1" }, "suggest": { "laminas/laminas-crypt": "^3.8 Crammd5 support in SMTP Auth", @@ -2764,32 +3013,32 @@ "type": "community_bridge" } ], - "time": "2022-10-14T13:05:29+00:00" + "time": "2022-12-05T18:42:59+00:00" }, { "name": "laminas/laminas-math", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-math.git", - "reference": "146d8187ab247ae152e811a6704a953d43537381" + "reference": "5770fc632a3614f5526632a8b70f41b65130460e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-math/zipball/146d8187ab247ae152e811a6704a953d43537381", - "reference": "146d8187ab247ae152e811a6704a953d43537381", + "url": "https://api.github.com/repos/laminas/laminas-math/zipball/5770fc632a3614f5526632a8b70f41b65130460e", + "reference": "5770fc632a3614f5526632a8b70f41b65130460e", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-math": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", - "phpunit/phpunit": "^9.5.5" + "laminas/laminas-coding-standard": "~2.4.0", + "phpunit/phpunit": "~9.5.25" }, "suggest": { "ext-bcmath": "If using the bcmath functionality", @@ -2831,7 +3080,7 @@ "type": "community_bridge" } ], - "time": "2021-12-06T02:02:07+00:00" + "time": "2022-10-16T14:22:28+00:00" }, { "name": "laminas/laminas-mime", @@ -2968,16 +3217,16 @@ }, { "name": "laminas/laminas-mvc", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mvc.git", - "reference": "111e08a9c27274af570260c83abe77204ccf3366" + "reference": "c54eaebe3810feaca834cc38ef0a962c89ff2431" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mvc/zipball/111e08a9c27274af570260c83abe77204ccf3366", - "reference": "111e08a9c27274af570260c83abe77204ccf3366", + "url": "https://api.github.com/repos/laminas/laminas-mvc/zipball/c54eaebe3810feaca834cc38ef0a962c89ff2431", + "reference": "c54eaebe3810feaca834cc38ef0a962c89ff2431", "shasum": "" }, "require": { @@ -3047,20 +3296,20 @@ "type": "community_bridge" } ], - "time": "2022-10-21T14:19:57+00:00" + "time": "2022-12-05T14:02:56+00:00" }, { "name": "laminas/laminas-oauth", - "version": "2.4.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-oauth.git", - "reference": "8075a5c6b17523cb262ed3a6a3764b3cbf84d781" + "reference": "882daa922f3d4f3c1a4282d5c0afeddabefaadb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-oauth/zipball/8075a5c6b17523cb262ed3a6a3764b3cbf84d781", - "reference": "8075a5c6b17523cb262ed3a6a3764b3cbf84d781", + "url": "https://api.github.com/repos/laminas/laminas-oauth/zipball/882daa922f3d4f3c1a4282d5c0afeddabefaadb9", + "reference": "882daa922f3d4f3c1a4282d5c0afeddabefaadb9", "shasum": "" }, "require": { @@ -3072,7 +3321,7 @@ "laminas/laminas-math": "^3.5", "laminas/laminas-stdlib": "^3.10", "laminas/laminas-uri": "^2.9", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zendoauth": "*" @@ -3109,20 +3358,20 @@ "type": "community_bridge" } ], - "time": "2022-07-22T12:18:05+00:00" + "time": "2022-11-17T10:40:56+00:00" }, { "name": "laminas/laminas-permissions-acl", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-permissions-acl.git", - "reference": "0d88f430953fbcbce382f09090db28905b90d60f" + "reference": "a13454dc3013cdcb388c95c418866e93dc781300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-permissions-acl/zipball/0d88f430953fbcbce382f09090db28905b90d60f", - "reference": "0d88f430953fbcbce382f09090db28905b90d60f", + "url": "https://api.github.com/repos/laminas/laminas-permissions-acl/zipball/a13454dc3013cdcb388c95c418866e93dc781300", + "reference": "a13454dc3013cdcb388c95c418866e93dc781300", "shasum": "" }, "require": { @@ -3135,9 +3384,9 @@ "require-dev": { "laminas/laminas-coding-standard": "~2.4.0", "laminas/laminas-servicemanager": "^3.19", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.29" + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0" }, "suggest": { "laminas/laminas-servicemanager": "To support Laminas\\Permissions\\Acl\\Assertion\\AssertionManager plugin manager usage" @@ -3172,37 +3421,38 @@ "type": "community_bridge" } ], - "time": "2022-10-17T04:26:35+00:00" + "time": "2022-12-01T10:29:36+00:00" }, { "name": "laminas/laminas-recaptcha", - "version": "3.4.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-recaptcha.git", - "reference": "f3bdb2fcaf859b9f725f397dc1bc38b4a7696a71" + "reference": "ead14136a0ded44d1a72f4885df0f3333065d919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-recaptcha/zipball/f3bdb2fcaf859b9f725f397dc1bc38b4a7696a71", - "reference": "f3bdb2fcaf859b9f725f397dc1bc38b4a7696a71", + "url": "https://api.github.com/repos/laminas/laminas-recaptcha/zipball/ead14136a0ded44d1a72f4885df0f3333065d919", + "reference": "ead14136a0ded44d1a72f4885df0f3333065d919", "shasum": "" }, "require": { "ext-json": "*", "laminas/laminas-http": "^2.15", - "laminas/laminas-json": "^3.3", - "laminas/laminas-stdlib": "^3.6", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-stdlib": "^3.10.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zendservice-recaptcha": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.3.0", + "laminas/laminas-coding-standard": "~2.4.0", "laminas/laminas-config": "^3.7", "laminas/laminas-validator": "^2.15", - "phpunit/phpunit": "^9.5.4" + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0.0" }, "suggest": { "laminas/laminas-validator": "~2.0, if using ReCaptcha's Mailhide API" @@ -3237,41 +3487,40 @@ "type": "community_bridge" } ], - "time": "2021-11-28T18:10:25+00:00" + "time": "2022-12-05T21:28:54+00:00" }, { "name": "laminas/laminas-router", - "version": "3.5.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-router.git", - "reference": "44759e71620030c93d99e40b394fe9fff8f0beda" + "reference": "48b6fccd63b9e04e67781c212bf3bedd75c9ca17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-router/zipball/44759e71620030c93d99e40b394fe9fff8f0beda", - "reference": "44759e71620030c93d99e40b394fe9fff8f0beda", + "url": "https://api.github.com/repos/laminas/laminas-router/zipball/48b6fccd63b9e04e67781c212bf3bedd75c9ca17", + "reference": "48b6fccd63b9e04e67781c212bf3bedd75c9ca17", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.2", "laminas/laminas-http": "^2.15", - "laminas/laminas-servicemanager": "^3.7", - "laminas/laminas-stdlib": "^3.6", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-servicemanager": "^3.14.0", + "laminas/laminas-stdlib": "^3.10.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-router": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.2.1", - "laminas/laminas-i18n": "^2.7.4", - "phpunit/phpunit": "^9.5.5", - "psalm/plugin-phpunit": "^0.15.1", - "vimeo/psalm": "^4.7" + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-i18n": "^2.19.0", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0.0" }, "suggest": { - "laminas/laminas-i18n": "^2.7.4, if defining translatable HTTP path segments" + "laminas/laminas-i18n": "^2.15.0 if defining translatable HTTP path segments" }, "type": "library", "extra": { @@ -3309,33 +3558,33 @@ "type": "community_bridge" } ], - "time": "2021-10-13T16:02:43+00:00" + "time": "2022-12-02T17:45:59+00:00" }, { "name": "laminas/laminas-server", - "version": "2.11.1", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-server.git", - "reference": "f45e1a6f614a11af8eff5d2d409f12229101cfc1" + "reference": "7f4862913ab95ea5decd08e6c3717edbb398fde8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-server/zipball/f45e1a6f614a11af8eff5d2d409f12229101cfc1", - "reference": "f45e1a6f614a11af8eff5d2d409f12229101cfc1", + "url": "https://api.github.com/repos/laminas/laminas-server/zipball/7f4862913ab95ea5decd08e6c3717edbb398fde8", + "reference": "7f4862913ab95ea5decd08e6c3717edbb398fde8", "shasum": "" }, "require": { - "laminas/laminas-code": "^3.5.1 || ^4.0.0", + "laminas/laminas-code": "^4.7.1", "laminas/laminas-stdlib": "^3.3.1", "laminas/laminas-zendframework-bridge": "^1.2.0", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "replace": { "zendframework/zend-server": "^2.8.1" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-coding-standard": "~2.4.0", "phpunit/phpunit": "^9.5.5", "psalm/plugin-phpunit": "^0.15.1", "vimeo/psalm": "^4.6.4" @@ -3370,20 +3619,20 @@ "type": "community_bridge" } ], - "time": "2022-02-25T14:41:51+00:00" + "time": "2022-12-27T17:14:59+00:00" }, { "name": "laminas/laminas-servicemanager", - "version": "3.19.0", + "version": "3.20.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-servicemanager.git", - "reference": "ed160729bb8721127efdaac799f9a298963345b1" + "reference": "bc2c2cbe2dd90db8b9d16b0618f542692b76ab59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/ed160729bb8721127efdaac799f9a298963345b1", - "reference": "ed160729bb8721127efdaac799f9a298963345b1", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/bc2c2cbe2dd90db8b9d16b0618f542692b76ab59", + "reference": "bc2c2cbe2dd90db8b9d16b0618f542692b76ab59", "shasum": "" }, "require": { @@ -3406,14 +3655,14 @@ "require-dev": { "composer/package-versions-deprecated": "^1.11.99.5", "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-container-config-test": "^0.7", + "laminas/laminas-container-config-test": "^0.8", "laminas/laminas-dependency-plugin": "^2.2", "mikey179/vfsstream": "^1.6.11@alpha", "ocramius/proxy-manager": "^2.14.1", - "phpbench/phpbench": "^1.2.6", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.28" + "phpbench/phpbench": "^1.2.7", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0.0" }, "suggest": { "ocramius/proxy-manager": "ProxyManager ^2.1.1 to handle lazy initialization of services" @@ -3460,43 +3709,42 @@ "type": "community_bridge" } ], - "time": "2022-10-10T20:59:22+00:00" + "time": "2022-12-01T17:03:38+00:00" }, { "name": "laminas/laminas-session", - "version": "2.12.1", + "version": "2.16.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-session.git", - "reference": "888c6a344e9a4c9f34ab6e09346640eac9be3fcf" + "reference": "9c845a0361625d5775cad6f043716196201ad41f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-session/zipball/888c6a344e9a4c9f34ab6e09346640eac9be3fcf", - "reference": "888c6a344e9a4c9f34ab6e09346640eac9be3fcf", + "url": "https://api.github.com/repos/laminas/laminas-session/zipball/9c845a0361625d5775cad6f043716196201ad41f", + "reference": "9c845a0361625d5775cad6f043716196201ad41f", "shasum": "" }, "require": { - "laminas/laminas-eventmanager": "^3.4", - "laminas/laminas-stdlib": "^3.6", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-eventmanager": "^3.5", + "laminas/laminas-servicemanager": "^3.15.1", + "laminas/laminas-stdlib": "^3.10.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-session": "*" }, "require-dev": { - "container-interop/container-interop": "^1.1", - "laminas/laminas-cache": "3.0.x-dev", - "laminas/laminas-cache-storage-adapter-memory": "2.0.x-dev", - "laminas/laminas-coding-standard": "~2.2.1", - "laminas/laminas-db": "^2.13.4", - "laminas/laminas-http": "^2.15", - "laminas/laminas-servicemanager": "^3.7", - "laminas/laminas-validator": "^2.15", - "mongodb/mongodb": "v1.9.x-dev", - "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5.9" + "laminas/laminas-cache": "^3.8", + "laminas/laminas-cache-storage-adapter-memory": "^2.2", + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-db": "^2.15", + "laminas/laminas-http": "^2.17.1", + "laminas/laminas-validator": "^2.28", + "mongodb/mongodb": "~1.13.0", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component", @@ -3542,40 +3790,42 @@ "type": "community_bridge" } ], - "time": "2022-02-15T16:38:29+00:00" + "time": "2022-12-04T11:15:36+00:00" }, { "name": "laminas/laminas-soap", - "version": "2.10.0", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-soap.git", - "reference": "b1245a09b523485060407f73a0058fb871d2c656" + "reference": "127de3d876b992a6327c274b15df6de26d7aa712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-soap/zipball/b1245a09b523485060407f73a0058fb871d2c656", - "reference": "b1245a09b523485060407f73a0058fb871d2c656", + "url": "https://api.github.com/repos/laminas/laminas-soap/zipball/127de3d876b992a6327c274b15df6de26d7aa712", + "reference": "127de3d876b992a6327c274b15df6de26d7aa712", "shasum": "" }, "require": { "ext-dom": "*", "ext-soap": "*", - "laminas/laminas-server": "^2.11", - "laminas/laminas-stdlib": "^3.6", - "laminas/laminas-uri": "^2.9.1", - "php": "~7.4.0 || ~8.0.0 || ~8.1.0" + "laminas/laminas-server": "^2.15", + "laminas/laminas-stdlib": "^3.16", + "laminas/laminas-uri": "^2.10", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "laminas/laminas-code": "<4.4", "zendframework/zend-soap": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.2.1", - "laminas/laminas-config": "^3.7", - "laminas/laminas-http": "^2.15", + "laminas/laminas-coding-standard": "^2.5", + "laminas/laminas-config": "^3.8", + "laminas/laminas-http": "^2.18", "phpspec/prophecy-phpunit": "^2.0.1", - "phpunit/phpunit": "^9.5.5" + "phpunit/phpunit": "^9.5.27", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^4.30" }, "suggest": { "ext-curl": "Curl is required when .NET compatibility is required", @@ -3610,20 +3860,20 @@ "type": "community_bridge" } ], - "time": "2021-10-14T14:04:27+00:00" + "time": "2023-01-09T13:58:49+00:00" }, { "name": "laminas/laminas-stdlib", - "version": "3.15.0", + "version": "3.16.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "63b66bd4b696f024f42616b9d95cdb10e5109c27" + "reference": "f4f773641807c7ccee59b758bfe4ac4ba33ecb17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/63b66bd4b696f024f42616b9d95cdb10e5109c27", - "reference": "63b66bd4b696f024f42616b9d95cdb10e5109c27", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/f4f773641807c7ccee59b758bfe4ac4ba33ecb17", + "reference": "f4f773641807c7ccee59b758bfe4ac4ba33ecb17", "shasum": "" }, "require": { @@ -3634,10 +3884,10 @@ }, "require-dev": { "laminas/laminas-coding-standard": "^2.4.0", - "phpbench/phpbench": "^1.2.6", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.28" + "phpbench/phpbench": "^1.2.7", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0.0" }, "type": "library", "autoload": { @@ -3669,33 +3919,35 @@ "type": "community_bridge" } ], - "time": "2022-10-10T19:10:24+00:00" + "time": "2022-12-03T18:48:01+00:00" }, { "name": "laminas/laminas-text", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-text.git", - "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7" + "reference": "40f7acdb284d41553d32db811e704d6e15e415b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-text/zipball/8879e75d03e09b0d6787e6680cfa255afd4645a7", - "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7", + "url": "https://api.github.com/repos/laminas/laminas-text/zipball/40f7acdb284d41553d32db811e704d6e15e415b4", + "reference": "40f7acdb284d41553d32db811e704d6e15e415b4", "shasum": "" }, "require": { - "laminas/laminas-servicemanager": "^3.4", - "laminas/laminas-stdlib": "^3.6", - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "laminas/laminas-servicemanager": "^3.19.0", + "laminas/laminas-stdlib": "^3.7.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "conflict": { "zendframework/zend-text": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", - "phpunit/phpunit": "^9.3" + "laminas/laminas-coding-standard": "~2.4.0", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.1" }, "type": "library", "autoload": { @@ -3727,7 +3979,7 @@ "type": "community_bridge" } ], - "time": "2021-09-02T16:50:53+00:00" + "time": "2022-12-11T15:36:27+00:00" }, { "name": "laminas/laminas-uri", @@ -3789,40 +4041,40 @@ }, { "name": "laminas/laminas-validator", - "version": "2.26.0", + "version": "2.29.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-validator.git", - "reference": "a995b21d18c63cd1f5d123d0d2cd31a1c2d828dc" + "reference": "e40ee8d86cc1907083e273bfd6ed8b6dde2d9850" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/a995b21d18c63cd1f5d123d0d2cd31a1c2d828dc", - "reference": "a995b21d18c63cd1f5d123d0d2cd31a1c2d828dc", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/e40ee8d86cc1907083e273bfd6ed8b6dde2d9850", + "reference": "e40ee8d86cc1907083e273bfd6ed8b6dde2d9850", "shasum": "" }, "require": { "laminas/laminas-servicemanager": "^3.12.0", "laminas/laminas-stdlib": "^3.13", - "php": "~8.0.0 || ~8.1.0 || ~8.2.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "psr/http-message": "^1.0.1" }, "conflict": { "zendframework/zend-validator": "*" }, "require-dev": { "laminas/laminas-coding-standard": "^2.4.0", - "laminas/laminas-db": "^2.15.0", - "laminas/laminas-filter": "^2.22", - "laminas/laminas-http": "^2.16.0", + "laminas/laminas-db": "^2.16", + "laminas/laminas-filter": "^2.28.1", + "laminas/laminas-http": "^2.18", "laminas/laminas-i18n": "^2.19", - "laminas/laminas-session": "^2.13.0", - "laminas/laminas-uri": "^2.9.1", - "phpunit/phpunit": "^9.5.25", - "psalm/plugin-phpunit": "^0.17.0", + "laminas/laminas-session": "^2.15", + "laminas/laminas-uri": "^2.10.0", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.3", "psr/http-client": "^1.0.1", "psr/http-factory": "^1.0.1", - "psr/http-message": "^1.0.1", - "vimeo/psalm": "^4.28" + "vimeo/psalm": "^5.0" }, "suggest": { "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", @@ -3870,68 +4122,63 @@ "type": "community_bridge" } ], - "time": "2022-10-11T12:58:36+00:00" + "time": "2022-12-13T22:53:38+00:00" }, { "name": "laminas/laminas-view", - "version": "2.20.0", + "version": "2.25.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-view.git", - "reference": "2cd6973a3e042be3d244260fe93f435668f5c2b4" + "reference": "77a4b6d78445ae2f30625c5af09a05ad4e4434eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-view/zipball/2cd6973a3e042be3d244260fe93f435668f5c2b4", - "reference": "2cd6973a3e042be3d244260fe93f435668f5c2b4", + "url": "https://api.github.com/repos/laminas/laminas-view/zipball/77a4b6d78445ae2f30625c5af09a05ad4e4434eb", + "reference": "77a4b6d78445ae2f30625c5af09a05ad4e4434eb", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.2", "ext-dom": "*", "ext-filter": "*", "ext-json": "*", "laminas/laminas-escaper": "^2.5", "laminas/laminas-eventmanager": "^3.4", "laminas/laminas-json": "^3.3", - "laminas/laminas-servicemanager": "^3.10", - "laminas/laminas-stdlib": "^3.6", - "php": "^7.4 || ~8.0.0 || ~8.1.0", + "laminas/laminas-servicemanager": "^3.14.0", + "laminas/laminas-stdlib": "^3.10.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "psr/container": "^1 || ^2" }, "conflict": { "container-interop/container-interop": "<1.2", "laminas/laminas-router": "<3.0.1", - "laminas/laminas-servicemanager": "<3.3", "laminas/laminas-session": "<2.12", "zendframework/zend-view": "*" }, "require-dev": { - "laminas/laminas-authentication": "^2.5", - "laminas/laminas-coding-standard": "~2.3.0", - "laminas/laminas-console": "^2.6", - "laminas/laminas-feed": "^2.15", - "laminas/laminas-filter": "^2.13.0", - "laminas/laminas-http": "^2.15", - "laminas/laminas-i18n": "^2.6", - "laminas/laminas-modulemanager": "^2.7.1", - "laminas/laminas-mvc": "^3.0", - "laminas/laminas-mvc-i18n": "^1.1", - "laminas/laminas-mvc-plugin-flashmessenger": "^1.5.0", - "laminas/laminas-navigation": "^2.13.1", - "laminas/laminas-paginator": "^2.11.0", - "laminas/laminas-permissions-acl": "^2.6", - "laminas/laminas-router": "^3.0.1", - "laminas/laminas-uri": "^2.5", - "phpspec/prophecy": "^1.12", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5.5", - "psalm/plugin-phpunit": "^0.16.1", - "vimeo/psalm": "^4.10" + "laminas/laminas-authentication": "^2.13", + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-console": "^2.8", + "laminas/laminas-feed": "^2.19", + "laminas/laminas-filter": "^2.25", + "laminas/laminas-http": "^2.17", + "laminas/laminas-i18n": "^2.19", + "laminas/laminas-modulemanager": "^2.14", + "laminas/laminas-mvc": "^3.5", + "laminas/laminas-mvc-i18n": "^1.6", + "laminas/laminas-mvc-plugin-flashmessenger": "^1.9", + "laminas/laminas-navigation": "^2.16", + "laminas/laminas-paginator": "^2.15", + "laminas/laminas-permissions-acl": "^2.12", + "laminas/laminas-router": "^3.10", + "laminas/laminas-uri": "^2.10", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.30" }, "suggest": { "laminas/laminas-authentication": "Laminas\\Authentication component", - "laminas/laminas-escaper": "Laminas\\Escaper component", "laminas/laminas-feed": "Laminas\\Feed component", "laminas/laminas-filter": "Laminas\\Filter component", "laminas/laminas-http": "Laminas\\Http component", @@ -3941,7 +4188,6 @@ "laminas/laminas-navigation": "Laminas\\Navigation component", "laminas/laminas-paginator": "Laminas\\Paginator component", "laminas/laminas-permissions-acl": "Laminas\\Permissions\\Acl component", - "laminas/laminas-servicemanager": "Laminas\\ServiceManager component", "laminas/laminas-uri": "Laminas\\Uri component" }, "bin": [ @@ -3977,30 +4223,30 @@ "type": "community_bridge" } ], - "time": "2022-02-22T13:52:44+00:00" + "time": "2022-11-07T08:01:13+00:00" }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.5.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "7f049390b756d34ba5940a8fb47634fbb51f79ab" + "reference": "5ef52e26392777a26dbb8f20fe24f91b406459f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/7f049390b756d34ba5940a8fb47634fbb51f79ab", - "reference": "7f049390b756d34ba5940a8fb47634fbb51f79ab", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/5ef52e26392777a26dbb8f20fe24f91b406459f6", + "reference": "5ef52e26392777a26dbb8f20fe24f91b406459f6", "shasum": "" }, "require": { - "php": ">=7.4, <8.2" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.14", - "psalm/plugin-phpunit": "^0.15.2", - "squizlabs/php_codesniffer": "^3.6.2", - "vimeo/psalm": "^4.21.0" + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "squizlabs/php_codesniffer": "^3.7.1", + "vimeo/psalm": "^4.29.0" }, "type": "library", "extra": { @@ -4039,20 +4285,20 @@ "type": "community_bridge" } ], - "time": "2022-02-22T22:17:01+00:00" + "time": "2022-12-12T11:44:10+00:00" }, { "name": "league/flysystem", - "version": "2.4.5", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "9392c5f1df57d865c406ee65e5012d566686be12" + "reference": "8aaffb653c5777781b0f7f69a5d937baf7ab6cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9392c5f1df57d865c406ee65e5012d566686be12", - "reference": "9392c5f1df57d865c406ee65e5012d566686be12", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/8aaffb653c5777781b0f7f69a5d937baf7ab6cdb", + "reference": "8aaffb653c5777781b0f7f69a5d937baf7ab6cdb", "shasum": "" }, "require": { @@ -4109,11 +4355,11 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/2.4.5" + "source": "https://github.com/thephpleague/flysystem/tree/2.5.0" }, "funding": [ { - "url": "https://offset.earth/frankdejonge", + "url": "https://ecologi.com/frankdejonge", "type": "custom" }, { @@ -4125,20 +4371,20 @@ "type": "tidelift" } ], - "time": "2022-04-25T18:39:39+00:00" + "time": "2022-09-17T21:02:32+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "2.4.3", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "bf8c03f9c1c8a69f7fd2854d57127840e1b6ccd2" + "reference": "2ae435f7177fd5d3afc0090bc7f849093d8361e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/bf8c03f9c1c8a69f7fd2854d57127840e1b6ccd2", - "reference": "bf8c03f9c1c8a69f7fd2854d57127840e1b6ccd2", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/2ae435f7177fd5d3afc0090bc7f849093d8361e8", + "reference": "2ae435f7177fd5d3afc0090bc7f849093d8361e8", "shasum": "" }, "require": { @@ -4178,9 +4424,23 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/2.4.3" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/2.5.0" }, - "time": "2022-02-16T18:40:49+00:00" + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2022-09-09T19:33:51+00:00" }, { "name": "league/mime-type-detection", @@ -4240,21 +4500,21 @@ }, { "name": "magento/composer", - "version": "1.9.0-beta1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/magento/composer.git", - "reference": "df4458651a0bd075a3fe9856c4d2384b8e37f94b" + "reference": "71274ccec4abc54c42c5fc8f59d91401defbc99c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/composer/zipball/df4458651a0bd075a3fe9856c4d2384b8e37f94b", - "reference": "df4458651a0bd075a3fe9856c4d2384b8e37f94b", + "url": "https://api.github.com/repos/magento/composer/zipball/71274ccec4abc54c42c5fc8f59d91401defbc99c", + "reference": "71274ccec4abc54c42c5fc8f59d91401defbc99c", "shasum": "" }, "require": { - "composer/composer": "^1.9 || ^2.0", - "php": "~7.4.0||~8.1.0", + "composer/composer": "^2.0", + "php": "~7.4.0||~8.1.0||~8.2.0", "symfony/console": "~4.4.0||~5.4.0" }, "require-dev": { @@ -4274,9 +4534,9 @@ "description": "Magento composer library helps to instantiate Composer application and run composer commands.", "support": { "issues": "https://github.com/magento/composer/issues", - "source": "https://github.com/magento/composer/tree/1.9.0-beta1" + "source": "https://github.com/magento/composer/tree/1.9.0" }, - "time": "2022-06-23T14:26:38+00:00" + "time": "2023-02-15T20:41:53+00:00" }, { "name": "magento/composer-dependency-version-audit-plugin", @@ -4322,21 +4582,22 @@ }, { "name": "magento/magento-composer-installer", - "version": "0.4.0-beta1", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/magento/magento-composer-installer.git", - "reference": "dc7065e47acec3338f282ea679d9ee815cd807ac" + "reference": "85496104b065f5a7b8d824f37017c53dbbb93a44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-composer-installer/zipball/dc7065e47acec3338f282ea679d9ee815cd807ac", - "reference": "dc7065e47acec3338f282ea679d9ee815cd807ac", + "url": "https://api.github.com/repos/magento/magento-composer-installer/zipball/85496104b065f5a7b8d824f37017c53dbbb93a44", + "reference": "85496104b065f5a7b8d824f37017c53dbbb93a44", "shasum": "" }, "require": { "composer-plugin-api": "^1.1 || ^2.0", - "composer/composer": "^1.9 || ^2.0" + "composer/composer": "^1.9 || ^2.0", + "laminas/laminas-stdlib": "^3.11.0" }, "replace": { "magento-hackathon/magento-composer-installer": "*" @@ -4396,9 +4657,9 @@ "magento" ], "support": { - "source": "https://github.com/magento/magento-composer-installer/tree/0.4.0-beta1" + "source": "https://github.com/magento/magento-composer-installer/tree/0.4.0" }, - "time": "2022-06-27T21:45:22+00:00" + "time": "2022-12-01T15:21:32+00:00" }, { "name": "magento/zend-cache", @@ -4710,16 +4971,16 @@ }, { "name": "magento/zend-pdf", - "version": "1.16.0", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/magento/magento-zend-pdf.git", - "reference": "cb5179d708fb9c39d753d556f49471d3d0037aac" + "reference": "e69a4f0ab33ea1355701cebe6cb64bc02e642b33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-zend-pdf/zipball/cb5179d708fb9c39d753d556f49471d3d0037aac", - "reference": "cb5179d708fb9c39d753d556f49471d3d0037aac", + "url": "https://api.github.com/repos/magento/magento-zend-pdf/zipball/e69a4f0ab33ea1355701cebe6cb64bc02e642b33", + "reference": "e69a4f0ab33ea1355701cebe6cb64bc02e642b33", "shasum": "" }, "require": { @@ -4761,22 +5022,22 @@ ], "support": { "issues": "https://github.com/magento/magento-zend-pdf/issues", - "source": "https://github.com/magento/magento-zend-pdf/tree/1.16.0" + "source": "https://github.com/magento/magento-zend-pdf/tree/1.16.1" }, - "time": "2022-09-22T18:56:44+00:00" + "time": "2023-01-26T16:40:05+00:00" }, { "name": "monolog/monolog", - "version": "2.8.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "720488632c590286b88b80e62aa3d3d551ad4a50" + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/720488632c590286b88b80e62aa3d3d551ad4a50", - "reference": "720488632c590286b88b80e62aa3d3d551ad4a50", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1", "shasum": "" }, "require": { @@ -4791,7 +5052,7 @@ "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^7 || ^8", "ext-json": "*", - "graylog2/gelf-php": "^1.4.2", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", "guzzlehttp/guzzle": "^7.4", "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", @@ -4853,7 +5114,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.8.0" + "source": "https://github.com/Seldaek/monolog/tree/2.9.1" }, "funding": [ { @@ -4865,7 +5126,7 @@ "type": "tidelift" } ], - "time": "2022-07-24T11:55:47+00:00" + "time": "2023-02-06T13:44:46+00:00" }, { "name": "mtdowling/jmespath.php", @@ -4930,16 +5191,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -4980,9 +5241,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "opensearch-project/opensearch-php", @@ -5050,16 +5311,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.5.0", + "version": "v2.6.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "9229e15f2e6ba772f0c55dd6986c563b937170a8" + "reference": "58c3f47f650c94ec05a151692652a868995d2938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9229e15f2e6ba772f0c55dd6986c563b937170a8", - "reference": "9229e15f2e6ba772f0c55dd6986c563b937170a8", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", "shasum": "" }, "require": { @@ -5113,7 +5374,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-01-17T05:32:27+00:00" + "time": "2022-06-14T06:56:20+00:00" }, { "name": "paragonie/random_compat", @@ -5167,34 +5428,34 @@ }, { "name": "pelago/emogrifier", - "version": "v6.0.0", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", - "reference": "aa72d5407efac118f3896bcb995a2cba793df0ae" + "reference": "547b8c814794aec871e3c98b1c712f416755f4eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/aa72d5407efac118f3896bcb995a2cba793df0ae", - "reference": "aa72d5407efac118f3896bcb995a2cba793df0ae", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/547b8c814794aec871e3c98b1c712f416755f4eb", + "reference": "547b8c814794aec871e3c98b1c712f416755f4eb", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", - "php": "~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0", - "sabberworm/php-css-parser": "^8.3.1", - "symfony/css-selector": "^3.4.32 || ^4.4 || ^5.3 || ^6.0" + "php": "~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "sabberworm/php-css-parser": "^8.4.0", + "symfony/css-selector": "^4.4.23 || ^5.4.0 || ^6.0.0" }, "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.3.0", - "phpunit/phpunit": "^8.5.16", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpunit/phpunit": "^9.5.25", "rawr/cross-data-providers": "^2.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0.x-dev" + "dev-main": "8.0.x-dev" } }, "autoload": { @@ -5241,20 +5502,20 @@ "issues": "https://github.com/MyIntervals/emogrifier/issues", "source": "https://github.com/MyIntervals/emogrifier" }, - "time": "2021-09-16T16:22:04+00:00" + "time": "2022-11-01T17:53:29+00:00" }, { "name": "php-amqplib/php-amqplib", - "version": "v3.2.0", + "version": "v3.5.3", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "0bec5b392428e0ac3b3f34fbc4e02d706995833e" + "reference": "bccaaf8ef8bcf18b4ab41e645e92537752b887bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/0bec5b392428e0ac3b3f34fbc4e02d706995833e", - "reference": "0bec5b392428e0ac3b3f34fbc4e02d706995833e", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/bccaaf8ef8bcf18b4ab41e645e92537752b887bd", + "reference": "bccaaf8ef8bcf18b4ab41e645e92537752b887bd", "shasum": "" }, "require": { @@ -5320,9 +5581,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.2.0" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.5.3" }, - "time": "2022-03-10T19:16:00+00:00" + "time": "2023-04-03T18:25:49+00:00" }, { "name": "phpseclib/mcrypt_compat", @@ -5394,16 +5655,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.14", + "version": "3.0.20", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef" + "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2f0b7af658cbea265cbb4a791d6c29a6613f98ef", - "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67", + "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67", "shasum": "" }, "require": { @@ -5415,6 +5676,7 @@ "phpunit/phpunit": "*" }, "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", @@ -5483,7 +5745,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.14" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.20" }, "funding": [ { @@ -5499,7 +5761,56 @@ "type": "tidelift" } ], - "time": "2022-04-04T05:15:45+00:00" + "time": "2023-06-13T06:30:34+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.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": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" }, { "name": "psr/container", @@ -5601,21 +5912,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -5635,7 +5946,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -5647,27 +5958,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client/tree/1.0.2" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-04-10T20:12:12+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -5687,7 +5998,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for PSR-7 HTTP message factories", @@ -5702,31 +6013,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -5755,9 +6066,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", @@ -5855,42 +6166,52 @@ }, { "name": "ramsey/collection", - "version": "1.2.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", - "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "shasum": "" }, "require": { - "php": "^7.3 || ^8", - "symfony/polyfill-php81": "^1.23" + "php": "^8.1" }, "require-dev": { - "captainhook/captainhook": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.6", - "fakerphp/faker": "^1.5", - "hamcrest/hamcrest-php": "^2", - "jangregor/phpstan-prophecy": "^0.8", - "mockery/mockery": "^1.3", + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1", - "phpstan/phpstan": "^0.12.32", - "phpstan/phpstan-mockery": "^0.12.5", - "phpstan/phpstan-phpunit": "^0.12.11", - "phpunit/phpunit": "^8.5 || ^9", - "psy/psysh": "^0.10.4", - "slevomat/coding-standard": "^6.3", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, "autoload": { "psr-4": { "Ramsey\\Collection\\": "src/" @@ -5918,7 +6239,7 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.2.2" + "source": "https://github.com/ramsey/collection/tree/2.0.0" }, "funding": [ { @@ -5930,29 +6251,27 @@ "type": "tidelift" } ], - "time": "2021-10-10T03:01:02+00:00" + "time": "2022-12-31T21:50:55+00:00" }, { "name": "ramsey/uuid", - "version": "4.2.3", + "version": "4.7.4", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + "reference": "60a4c63ab724854332900504274f6150ff26d286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", + "reference": "60a4c63ab724854332900504274f6150ff26d286", "shasum": "" }, "require": { - "brick/math": "^0.8 || ^0.9", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", "ext-json": "*", - "php": "^7.2 || ^8.0", - "ramsey/collection": "^1.0", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php80": "^1.14" + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" }, "replace": { "rhumsaa/uuid": "self.version" @@ -5964,24 +6283,23 @@ "doctrine/annotations": "^1.8", "ergebnis/composer-normalize": "^2.15", "mockery/mockery": "^1.3", - "moontoast/math": "^1.1", "paragonie/random-lib": "^2", "php-mock/php-mock": "^2.2", "php-mock/php-mock-mockery": "^1.3", "php-parallel-lint/php-parallel-lint": "^1.1", "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-mockery": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", "phpunit/phpunit": "^8.5 || ^9", - "slevomat/coding-standard": "^7.0", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.5", "vimeo/psalm": "^4.9" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-ctype": "Enables faster processing of character classification using ctype functions.", "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", @@ -5989,9 +6307,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "4.x-dev" - }, "captainhook": { "force-install": true } @@ -6016,7 +6331,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.2.3" + "source": "https://github.com/ramsey/uuid/tree/4.7.4" }, "funding": [ { @@ -6028,27 +6343,27 @@ "type": "tidelift" } ], - "time": "2021-09-25T23:10:38+00:00" + "time": "2023-04-15T23:01:58+00:00" }, { "name": "react/promise", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910" + "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910", - "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", + "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", "shasum": "" }, "require": { "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { @@ -6092,19 +6407,15 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.9.0" + "source": "https://github.com/reactphp/promise/tree/v2.10.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-02-11T10:27:51+00:00" + "time": "2023-05-02T15:15:43+00:00" }, { "name": "sabberworm/php-css-parser", @@ -6161,16 +6472,16 @@ }, { "name": "seld/jsonlint", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "4211420d25eba80712bff236a98960ef68b866b7" + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", - "reference": "4211420d25eba80712bff236a98960ef68b866b7", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1", + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1", "shasum": "" }, "require": { @@ -6209,7 +6520,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.0" }, "funding": [ { @@ -6221,7 +6532,7 @@ "type": "tidelift" } ], - "time": "2022-04-01T13:37:23+00:00" + "time": "2023-05-11T13:16:46+00:00" }, { "name": "seld/phar-utils", @@ -6271,6 +6582,67 @@ }, "time": "2022-08-31T10:31:18+00:00" }, + { + "name": "seld/signal-handler", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "f69d119511dc0360440cdbdaa71829c149b7be75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/f69d119511dc0360440cdbdaa71829c149b7be75", + "reference": "f69d119511dc0360440cdbdaa71829c149b7be75", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.1" + }, + "time": "2022-07-20T18:31:45+00:00" + }, { "name": "spomky-labs/aes-key-wrap", "version": "v7.0.0", @@ -6428,16 +6800,16 @@ }, { "name": "symfony/console", - "version": "v5.4.16", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef" + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8e9b9c8dfb33af6057c94e1b44846bee700dc5ef", - "reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef", + "url": "https://api.github.com/repos/symfony/console/zipball/560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", "shasum": "" }, "require": { @@ -6502,12 +6874,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.16" + "source": "https://github.com/symfony/console/tree/v5.4.24" }, "funding": [ { @@ -6523,25 +6895,24 @@ "type": "tidelift" } ], - "time": "2022-11-25T14:09:27+00:00" + "time": "2023-05-26T05:13:16+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.3", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e" + "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b0a190285cd95cb019237851205b8140ef6e368e", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", + "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -6573,7 +6944,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.3" + "source": "https://github.com/symfony/css-selector/tree/v6.3.0" }, "funding": [ { @@ -6589,7 +6960,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-03-20T16:43:42+00:00" }, { "name": "symfony/dependency-injection", @@ -6682,25 +7053,25 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -6729,7 +7100,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -6745,31 +7116,31 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/error-handler", - "version": "v6.2.5", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "0092696af0be8e6124b042fbe2890ca1788d7b28" + "reference": "c116cda1f51c678782768dce89a45f13c949455d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/0092696af0be8e6124b042fbe2890ca1788d7b28", - "reference": "0092696af0be8e6124b042fbe2890ca1788d7b28", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c116cda1f51c678782768dce89a45f13c949455d", + "reference": "c116cda1f51c678782768dce89a45f13c949455d", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=7.2.5", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "require-dev": { "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0" + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -6800,7 +7171,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.2.5" + "source": "https://github.com/symfony/error-handler/tree/v5.4.9" }, "funding": [ { @@ -6816,48 +7187,43 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:38:09+00:00" + "time": "2022-05-21T13:57:48+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.19", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c" + "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abf49cc084c087d94b4cb939c3f3672971784e0c", - "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", + "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0" }, "type": "library", "autoload": { @@ -6885,7 +7251,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.19" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.0" }, "funding": [ { @@ -6901,33 +7267,30 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-04-21T14:41:17+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae" + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0782b0b52a737a05b4383d0df35a474303cabdae", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -6964,7 +7327,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" }, "funding": [ { @@ -6980,27 +7343,26 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.13", + "version": "v6.3.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51" + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "~1.8" }, "type": "library", "autoload": { @@ -7028,7 +7390,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" }, "funding": [ { @@ -7044,20 +7406,20 @@ "type": "tidelift" } ], - "time": "2022-09-21T19:53:16+00:00" + "time": "2023-06-01T08:30:39+00:00" }, { "name": "symfony/finder", - "version": "v5.4.11", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" + "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", + "url": "https://api.github.com/repos/symfony/finder/zipball/078e9a5e1871fcfe6a5ce421b539344c21afef19", + "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19", "shasum": "" }, "require": { @@ -7091,7 +7453,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.11" + "source": "https://github.com/symfony/finder/tree/v5.4.21" }, "funding": [ { @@ -7107,20 +7469,20 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:37:50+00:00" + "time": "2023-02-16T09:33:00+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.20", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "d0435363362a47c14e9cf50663cb8ffbf491875a" + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0435363362a47c14e9cf50663cb8ffbf491875a", - "reference": "d0435363362a47c14e9cf50663cb8ffbf491875a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f66be2706075c5f6325d2fe2b743a57fb5d23f6b", + "reference": "f66be2706075c5f6325d2fe2b743a57fb5d23f6b", "shasum": "" }, "require": { @@ -7167,7 +7529,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.20" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.25" }, "funding": [ { @@ -7183,64 +7545,67 @@ "type": "tidelift" } ], - "time": "2023-01-29T11:11:52+00:00" + "time": "2023-06-22T08:06:06+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.0.20", + "version": "v5.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6dc70833fd0ef5e861e17c7854c12d7d86679349" + "reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6dc70833fd0ef5e861e17c7854c12d7d86679349", - "reference": "6dc70833fd0ef5e861e17c7854c12d7d86679349", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/255ae3b0a488d78fbb34da23d3e0c059874b5948", + "reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948", "shasum": "" }, "require": { - "php": ">=8.0.2", - "psr/log": "^1|^2|^3", - "symfony/error-handler": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/polyfill-ctype": "^1.8" + "php": ">=7.2.5", + "psr/log": "^1|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^5.0|^6.0", + "symfony/http-foundation": "^5.3.7|^6.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16" }, "conflict": { "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.4", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", - "symfony/twig-bridge": "<5.4", - "symfony/validator": "<5.4", + "symfony/cache": "<5.0", + "symfony/config": "<5.0", + "symfony/console": "<4.4", + "symfony/dependency-injection": "<5.3", + "symfony/doctrine-bridge": "<5.0", + "symfony/form": "<5.0", + "symfony/http-client": "<5.0", + "symfony/mailer": "<5.0", + "symfony/messenger": "<5.0", + "symfony/translation": "<5.0", + "symfony/twig-bridge": "<5.0", + "symfony/validator": "<5.0", "twig/twig": "<2.13" }, "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/dom-crawler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", + "symfony/config": "^5.0|^6.0", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/process": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/translation": "^5.4|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/translation": "^4.4|^5.0|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^2.13|^3.0.4" }, @@ -7276,7 +7641,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.0.20" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.10" }, "funding": [ { @@ -7292,7 +7657,7 @@ "type": "tidelift" } ], - "time": "2023-02-01T08:22:55+00:00" + "time": "2022-06-26T16:57:59+00:00" }, { "name": "symfony/intl", @@ -8039,16 +8404,16 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", "shasum": "" }, "require": { @@ -8057,7 +8422,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8098,7 +8463,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" }, "funding": [ { @@ -8114,20 +8479,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/process", - "version": "v5.4.11", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" + "reference": "e3c46cc5689c8782944274bb30702106ecbe3b64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", + "url": "https://api.github.com/repos/symfony/process/zipball/e3c46cc5689c8782944274bb30702106ecbe3b64", + "reference": "e3c46cc5689c8782944274bb30702106ecbe3b64", "shasum": "" }, "require": { @@ -8160,7 +8525,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.11" + "source": "https://github.com/symfony/process/tree/v5.4.24" }, "funding": [ { @@ -8176,7 +8541,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2023-05-17T11:26:05+00:00" }, { "name": "symfony/service-contracts", @@ -8263,16 +8628,16 @@ }, { "name": "symfony/string", - "version": "v5.4.15", + "version": "v5.4.22", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed" + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", + "url": "https://api.github.com/repos/symfony/string/zipball/8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", "shasum": "" }, "require": { @@ -8329,7 +8694,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.15" + "source": "https://github.com/symfony/string/tree/v5.4.22" }, "funding": [ { @@ -8345,20 +8710,20 @@ "type": "tidelift" } ], - "time": "2022-10-05T15:16:54+00:00" + "time": "2023-03-14T06:11:53+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.2.5", + "version": "v6.3.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "44b7b81749fd20c1bdf4946c041050e22bc8da27" + "reference": "c81268d6960ddb47af17391a27d222bd58cf0515" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/44b7b81749fd20c1bdf4946c041050e22bc8da27", - "reference": "44b7b81749fd20c1bdf4946c041050e22bc8da27", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c81268d6960ddb47af17391a27d222bd58cf0515", + "reference": "c81268d6960ddb47af17391a27d222bd58cf0515", "shasum": "" }, "require": { @@ -8366,7 +8731,6 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, "require-dev": { @@ -8376,11 +8740,6 @@ "symfony/uid": "^5.4|^6.0", "twig/twig": "^2.13|^3.0.4" }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" - }, "bin": [ "Resources/bin/var-dump-server" ], @@ -8417,7 +8776,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.2.5" + "source": "https://github.com/symfony/var-dumper/tree/v6.3.1" }, "funding": [ { @@ -8433,7 +8792,7 @@ "type": "tidelift" } ], - "time": "2023-01-20T17:45:48+00:00" + "time": "2023-06-21T12:08:28+00:00" }, { "name": "tedivm/jshrink", @@ -8853,37 +9212,41 @@ }, { "name": "webonyx/graphql-php", - "version": "v14.11.6", + "version": "v15.0.3", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "6070542725b61fc7d0654a8a9855303e5e157434" + "reference": "bfa78b44a93c00ebc9a1bc92497bc170a0e3b656" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/6070542725b61fc7d0654a8a9855303e5e157434", - "reference": "6070542725b61fc7d0654a8a9855303e5e157434", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/bfa78b44a93c00ebc9a1bc92497bc170a0e3b656", + "reference": "bfa78b44a93c00ebc9a1bc92497bc170a0e3b656", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "php": "^7.1 || ^8" + "php": "^7.4 || ^8" }, "require-dev": { - "amphp/amp": "^2.3", - "doctrine/coding-standard": "^6.0", - "nyholm/psr7": "^1.2", + "amphp/amp": "^2.6", + "dms/phpunit-arraysubset-asserts": "^0.4", + "ergebnis/composer-normalize": "^2.28", + "mll-lab/php-cs-fixer-config": "^4.4", + "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.82", - "phpstan/phpstan-phpunit": "0.12.18", - "phpstan/phpstan-strict-rules": "0.12.9", - "phpunit/phpunit": "^7.2 || ^8.5", - "psr/http-message": "^1.0", - "react/promise": "2.*", - "simpod/php-coveralls-mirror": "^3.0", - "squizlabs/php_codesniffer": "3.5.4" + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.9.14", + "phpstan/phpstan-phpunit": "1.3.3", + "phpstan/phpstan-strict-rules": "1.4.5", + "phpunit/phpunit": "^9.5", + "psr/http-message": "^1", + "react/http": "^1.6", + "react/promise": "^2.9", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6", + "thecodingmachine/safe": "^1.3 || ^2" }, "suggest": { "psr/http-message": "To use standard GraphQL server", @@ -8907,7 +9270,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v14.11.6" + "source": "https://github.com/webonyx/graphql-php/tree/v15.0.3" }, "funding": [ { @@ -8915,30 +9278,31 @@ "type": "open_collective" } ], - "time": "2022-04-13T16:25:32+00:00" + "time": "2023-02-02T21:59:56+00:00" }, { "name": "wikimedia/less.php", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/wikimedia/less.php.git", - "reference": "a486d78b9bd16b72f237fc6093aa56d69ce8bd13" + "reference": "47c4714c68c9006c87676d76c172a18e1d180f60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/less.php/zipball/a486d78b9bd16b72f237fc6093aa56d69ce8bd13", - "reference": "a486d78b9bd16b72f237fc6093aa56d69ce8bd13", + "url": "https://api.github.com/repos/wikimedia/less.php/zipball/47c4714c68c9006c87676d76c172a18e1d180f60", + "reference": "47c4714c68c9006c87676d76c172a18e1d180f60", "shasum": "" }, "require": { "php": ">=7.2.9" }, "require-dev": { - "mediawiki/mediawiki-codesniffer": "34.0.0", - "mediawiki/minus-x": "1.0.0", - "php-parallel-lint/php-console-highlighter": "0.5.0", - "php-parallel-lint/php-parallel-lint": "1.2.0", + "mediawiki/mediawiki-codesniffer": "39.0.0", + "mediawiki/mediawiki-phan-config": "0.11.1 || 0.12.0", + "mediawiki/minus-x": "1.1.1", + "php-parallel-lint/php-console-highlighter": "1.0.0", + "php-parallel-lint/php-parallel-lint": "1.3.2", "phpunit/phpunit": "^8.5" }, "bin": [ @@ -8971,7 +9335,7 @@ "homepage": "https://github.com/Mordred" } ], - "description": "PHP port of the Javascript version of LESS http://lesscss.org (Originally maintained by Josh Schmidt)", + "description": "PHP port of the LESS processor", "keywords": [ "css", "less", @@ -8982,42 +9346,43 @@ ], "support": { "issues": "https://github.com/wikimedia/less.php/issues", - "source": "https://github.com/wikimedia/less.php/tree/v3.1.0" + "source": "https://github.com/wikimedia/less.php/tree/v3.2.0" }, - "time": "2020-12-11T19:33:31+00:00" + "time": "2023-01-09T18:45:54+00:00" } ], "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.5.2", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "a6156aef942a4e4de0add34a73d066a9458cefc6" + "reference": "d28f6ba7139406974b977e5611bc65b59c492a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/a6156aef942a4e4de0add34a73d066a9458cefc6", - "reference": "a6156aef942a4e4de0add34a73d066a9458cefc6", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/d28f6ba7139406974b977e5611bc65b59c492a55", + "reference": "d28f6ba7139406974b977e5611bc65b59c492a55", "shasum": "" }, "require": { - "allure-framework/allure-php-api": "^1.3", - "codeception/codeception": "^2.5 | ^3 | ^4", + "allure-framework/allure-php-commons": "^2.3.1", + "codeception/codeception": "^5.0.3", "ext-json": "*", - "php": ">=7.1.3", - "symfony/filesystem": "^2.7 | ^3 | ^4 | ^5", - "symfony/finder": "^2.7 | ^3 | ^4 | ^5" + "php": "^8" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^7.2 | ^8 | ^9" + "psalm/plugin-phpunit": "^0.18.4", + "remorhaz/php-json-data": "^0.5.3", + "remorhaz/php-json-path": "^0.7.7", + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.12" }, "type": "library", "autoload": { - "psr-0": { - "Yandex": "src/" + "psr-4": { + "Qameta\\Allure\\Codeception\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9029,6 +9394,11 @@ "name": "Ivan Krutov", "email": "vania-pooh@aerokube.com", "role": "Developer" + }, + { + "name": "Edward Surov", + "email": "zoohie@gmail.com", + "role": "Developer" } ], "description": "Allure Codeception integration", @@ -9047,82 +9417,24 @@ "issues": "https://github.com/allure-framework/allure-codeception/issues", "source": "https://github.com/allure-framework/allure-codeception" }, - "time": "2021-06-04T13:24:36+00:00" - }, - { - "name": "allure-framework/allure-php-api", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/allure-framework/allure-php-api.git", - "reference": "50507f482d490f114054f2281cca487db47fa2bd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-php-api/zipball/50507f482d490f114054f2281cca487db47fa2bd", - "reference": "50507f482d490f114054f2281cca487db47fa2bd", - "shasum": "" - }, - "require": { - "jms/serializer": "^1 | ^2 | ^3", - "php": ">=7.1.3", - "ramsey/uuid": "^3 | ^4", - "symfony/mime": "^4.3 | ^5" - }, - "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9" - }, - "type": "library", - "autoload": { - "psr-0": { - "Yandex": [ - "src/", - "test/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Ivan Krutov", - "email": "vania-pooh@yandex-team.ru", - "role": "Developer" - } - ], - "description": "Allure PHP commons", - "homepage": "http://allure.qatools.ru/", - "keywords": [ - "allure", - "api", - "php", - "report" - ], - "support": { - "email": "allure@qameta.io", - "issues": "https://github.com/allure-framework/allure-php-api/issues", - "source": "https://github.com/allure-framework/allure-php-api" - }, - "time": "2021-11-15T13:15:20+00:00" + "time": "2023-05-31T14:10:46+00:00" }, { "name": "allure-framework/allure-php-commons", - "version": "v2.0.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-php-commons2.git", - "reference": "946e375e90cce9e43d1622890fb5a312ec8086bb" + "reference": "5d7ed5ab510339652163ca1473eb499d4b7ec488" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-php-commons2/zipball/946e375e90cce9e43d1622890fb5a312ec8086bb", - "reference": "946e375e90cce9e43d1622890fb5a312ec8086bb", + "url": "https://api.github.com/repos/allure-framework/allure-php-commons2/zipball/5d7ed5ab510339652163ca1473eb499d4b7ec488", + "reference": "5d7ed5ab510339652163ca1473eb499d4b7ec488", "shasum": "" }, "require": { - "doctrine/annotations": "^1.12", + "doctrine/annotations": "^1.12 || ^2", "ext-json": "*", "php": "^8", "psr/log": "^1 || ^2 || ^3", @@ -9133,10 +9445,10 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1", - "phpunit/phpunit": "^9.5.10", - "psalm/plugin-phpunit": "^0.16.1", - "squizlabs/php_codesniffer": "^3.6.2", - "vimeo/psalm": "^4.15" + "phpunit/phpunit": "^9.6.8", + "psalm/plugin-phpunit": "^0.18.4", + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.12" }, "type": "library", "autoload": { @@ -9175,20 +9487,20 @@ "issues": "https://github.com/allure-framework/allure-php-commons2/issues", "source": "https://github.com/allure-framework/allure-php-commons" }, - "time": "2021-12-28T12:03:10+00:00" + "time": "2023-05-30T10:55:43+00:00" }, { "name": "allure-framework/allure-phpunit", - "version": "v2.0.0", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-phpunit.git", - "reference": "3884842467bcba9607db9d7aa69b82dcf0424218" + "reference": "a08e0092cdddfc8ead1953cf5bddf80b48595109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/3884842467bcba9607db9d7aa69b82dcf0424218", - "reference": "3884842467bcba9607db9d7aa69b82dcf0424218", + "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/a08e0092cdddfc8ead1953cf5bddf80b48595109", + "reference": "a08e0092cdddfc8ead1953cf5bddf80b48595109", "shasum": "" }, "require": { @@ -9200,10 +9512,10 @@ "amphp/byte-stream": "<1.5.1" }, "require-dev": { - "brianium/paratest": "^6.4.1", - "psalm/plugin-phpunit": "^0.16.1", - "squizlabs/php_codesniffer": "^3.6.2", - "vimeo/psalm": "^4.16.1" + "brianium/paratest": "^6.8", + "psalm/plugin-phpunit": "^0.18.4", + "squizlabs/php_codesniffer": "^3.7.1", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -9243,7 +9555,7 @@ "issues": "https://github.com/allure-framework/allure-phpunit/issues", "source": "https://github.com/allure-framework/allure-phpunit" }, - "time": "2021-12-29T11:34:16+00:00" + "time": "2023-01-12T14:27:20+00:00" }, { "name": "beberlei/assert", @@ -9377,61 +9689,76 @@ }, { "name": "codeception/codeception", - "version": "4.1.31", + "version": "5.0.10", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "15524571ae0686a7facc2eb1f40f600e5bbce9e5" + "reference": "ed4af7fd4103cdd035916fbb8f35124edd2d018b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/15524571ae0686a7facc2eb1f40f600e5bbce9e5", - "reference": "15524571ae0686a7facc2eb1f40f600e5bbce9e5", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/ed4af7fd4103cdd035916fbb8f35124edd2d018b", + "reference": "ed4af7fd4103cdd035916fbb8f35124edd2d018b", "shasum": "" }, "require": { - "behat/gherkin": "^4.4.0", - "codeception/lib-asserts": "^1.0 | 2.0.*@dev", - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0", - "codeception/stub": "^2.0 | ^3.0 | ^4.0", + "behat/gherkin": "^4.6.2", + "codeception/lib-asserts": "^2.0", + "codeception/stub": "^4.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/psr7": "^1.4 | ^2.0", - "php": ">=5.6.0 <9.0", - "symfony/console": ">=2.7 <6.0", - "symfony/css-selector": ">=2.7 <6.0", - "symfony/event-dispatcher": ">=2.7 <6.0", - "symfony/finder": ">=2.7 <6.0", - "symfony/yaml": ">=2.7 <6.0" - }, - "require-dev": { - "codeception/module-asserts": "^1.0 | 2.0.*@dev", - "codeception/module-cli": "^1.0 | 2.0.*@dev", - "codeception/module-db": "^1.0 | 2.0.*@dev", - "codeception/module-filesystem": "^1.0 | 2.0.*@dev", - "codeception/module-phpbrowser": "^1.0 | 2.0.*@dev", - "codeception/specify": "~0.3", + "php": "^8.0", + "phpunit/php-code-coverage": "^9.2 || ^10.0", + "phpunit/php-text-template": "^2.0 || ^3.0", + "phpunit/php-timer": "^5.0.3 || ^6.0", + "phpunit/phpunit": "^9.5.20 || ^10.0", + "psy/psysh": "^0.11.2", + "sebastian/comparator": "^4.0.5 || ^5.0", + "sebastian/diff": "^4.0.3 || ^5.0", + "symfony/console": ">=4.4.24 <7.0", + "symfony/css-selector": ">=4.4.24 <7.0", + "symfony/event-dispatcher": ">=4.4.24 <7.0", + "symfony/finder": ">=4.4.24 <7.0", + "symfony/var-dumper": ">=4.4.24 < 7.0", + "symfony/yaml": ">=4.4.24 <7.0" + }, + "conflict": { + "codeception/lib-innerbrowser": "<3.1.3", + "codeception/module-filesystem": "<3.0", + "codeception/module-phpbrowser": "<2.5" + }, + "replace": { + "codeception/phpunit-wrapper": "*" + }, + "require-dev": { + "codeception/lib-innerbrowser": "*@dev", + "codeception/lib-web": "^1.0", + "codeception/module-asserts": "*@dev", + "codeception/module-cli": "*@dev", + "codeception/module-db": "*@dev", + "codeception/module-filesystem": "*@dev", + "codeception/module-phpbrowser": "*@dev", "codeception/util-universalframework": "*@dev", - "monolog/monolog": "~1.8", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <6.0", - "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0" + "ext-simplexml": "*", + "jetbrains/phpstorm-attributes": "^1.0", + "symfony/dotenv": ">=4.4.24 <7.0", + "symfony/process": ">=4.4.24 <7.0", + "vlucas/phpdotenv": "^5.1" }, "suggest": { "codeception/specify": "BDD-style code blocks", "codeception/verify": "BDD-style assertions", - "hoa/console": "For interactive console functionality", + "ext-simplexml": "For loading params from XML files", "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" + "symfony/dotenv": "For loading params from .env files", + "symfony/phpunit-bridge": "For phpunit-bridge support", + "vlucas/phpdotenv": "For loading params from .env files" }, "bin": [ "codecept" ], "type": "library", - "extra": { - "branch-alias": [] - }, "autoload": { "files": [ "functions.php" @@ -9439,7 +9766,10 @@ "psr-4": { "Codeception\\": "src/Codeception", "Codeception\\Extension\\": "ext" - } + }, + "classmap": [ + "src/PHPUnit/TestCase.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9448,12 +9778,12 @@ "authors": [ { "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" + "email": "davert.ua@gmail.com", + "homepage": "https://codeception.com" } ], "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "BDD", "TDD", @@ -9463,7 +9793,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.1.31" + "source": "https://github.com/Codeception/Codeception/tree/5.0.10" }, "funding": [ { @@ -9471,26 +9801,26 @@ "type": "open_collective" } ], - "time": "2022-03-13T17:07:08+00:00" + "time": "2023-03-14T07:21:10+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.13.2", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" + "reference": "b8c7dff552249e560879c682ba44a4b963af91bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/b8c7dff552249e560879c682ba44a4b963af91bc", + "reference": "b8c7dff552249e560879c682ba44a4b963af91bc", "shasum": "" }, "require": { - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", + "codeception/phpunit-wrapper": "^7.7.1 | ^8.0.3 | ^9.0", "ext-dom": "*", - "php": ">=5.6.0 <9.0" + "php": "^7.4 | ^8.0" }, "type": "library", "autoload": { @@ -9523,31 +9853,84 @@ ], "support": { "issues": "https://github.com/Codeception/lib-asserts/issues", - "source": "https://github.com/Codeception/lib-asserts/tree/1.13.2" + "source": "https://github.com/Codeception/lib-asserts/tree/2.1.0" + }, + "time": "2023-02-10T18:36:23+00:00" + }, + { + "name": "codeception/lib-web", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Codeception/lib-web.git", + "reference": "f488ff9bc08c8985d43796db28da0bd18813bcae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/lib-web/zipball/f488ff9bc08c8985d43796db28da0bd18813bcae", + "reference": "f488ff9bc08c8985d43796db28da0bd18813bcae", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "guzzlehttp/psr7": "^2.0", + "php": "^8.0", + "symfony/css-selector": ">=4.4.24 <7.0" + }, + "conflict": { + "codeception/codeception": "<5.0.0-alpha3" + }, + "require-dev": { + "php-webdriver/webdriver": "^1.12", + "phpunit/phpunit": "^9.5 | ^10.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gintautas Miselis" + } + ], + "description": "Library containing files used by module-webdriver and lib-innerbrowser or module-phpbrowser", + "homepage": "https://codeception.com/", + "keywords": [ + "codeception" + ], + "support": { + "issues": "https://github.com/Codeception/lib-web/issues", + "source": "https://github.com/Codeception/lib-web/tree/1.0.2" }, - "time": "2020-10-21T16:26:20+00:00" + "time": "2023-04-18T20:32:51+00:00" }, { "name": "codeception/module-asserts", - "version": "1.3.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/1b6b150b30586c3614e7e5761b31834ed7968603", + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603", "shasum": "" }, "require": { "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.13.1", - "php": ">=5.6.0 <9.0" + "codeception/lib-asserts": "^2.0", + "php": "^8.0" }, "conflict": { - "codeception/codeception": "<4.0" + "codeception/codeception": "<5.0" }, "type": "library", "autoload": { @@ -9580,30 +9963,33 @@ ], "support": { "issues": "https://github.com/Codeception/module-asserts/issues", - "source": "https://github.com/Codeception/module-asserts/tree/1.3.1" + "source": "https://github.com/Codeception/module-asserts/tree/3.0.0" }, - "time": "2020-10-21T16:48:15+00:00" + "time": "2022-02-16T19:48:08+00:00" }, { "name": "codeception/module-sequence", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-sequence.git", - "reference": "b75be26681ae90824cde8f8df785981f293667e1" + "reference": "9738e2eb06035a0975171a0aa3fce00d284b4dfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-sequence/zipball/b75be26681ae90824cde8f8df785981f293667e1", - "reference": "b75be26681ae90824cde8f8df785981f293667e1", + "url": "https://api.github.com/repos/Codeception/module-sequence/zipball/9738e2eb06035a0975171a0aa3fce00d284b4dfb", + "reference": "9738e2eb06035a0975171a0aa3fce00d284b4dfb", "shasum": "" }, "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0" + "codeception/codeception": "^5.0", + "php": "^8.0" }, "type": "library", "autoload": { + "files": [ + "src/Codeception/Util/sq.php" + ], "classmap": [ "src/" ] @@ -9618,34 +10004,39 @@ } ], "description": "Sequence module for Codeception", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "codeception" ], "support": { "issues": "https://github.com/Codeception/module-sequence/issues", - "source": "https://github.com/Codeception/module-sequence/tree/1.0.1" + "source": "https://github.com/Codeception/module-sequence/tree/3.0.0" }, - "time": "2020-10-31T18:36:26+00:00" + "time": "2022-05-31T05:45:36+00:00" }, { "name": "codeception/module-webdriver", - "version": "1.4.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "baa18b7bf70aa024012f967b5ce5021e1faa9151" + "reference": "59b6fe426dddbe889c23593f8698c52e08bba6e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/baa18b7bf70aa024012f967b5ce5021e1faa9151", - "reference": "baa18b7bf70aa024012f967b5ce5021e1faa9151", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/59b6fe426dddbe889c23593f8698c52e08bba6e9", + "reference": "59b6fe426dddbe889c23593f8698c52e08bba6e9", "shasum": "" }, "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0", - "php-webdriver/webdriver": "^1.8.0" + "codeception/codeception": "^5.0.0-RC2", + "codeception/lib-web": "^1.0.1", + "codeception/stub": "^4.0", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0", + "php-webdriver/webdriver": "^1.8.0", + "phpunit/phpunit": "^9.5" }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" @@ -9672,7 +10063,7 @@ } ], "description": "WebDriver module for Codeception", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "acceptance-testing", "browser-testing", @@ -9680,77 +10071,31 @@ ], "support": { "issues": "https://github.com/Codeception/module-webdriver/issues", - "source": "https://github.com/Codeception/module-webdriver/tree/1.4.0" - }, - "time": "2021-09-02T12:01:02+00:00" - }, - { - "name": "codeception/phpunit-wrapper", - "version": "9.0.9", - "source": { - "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "7439a53ae367986e9c22b2ac00f9d7376bb2f8cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7439a53ae367986e9c22b2ac00f9d7376bb2f8cf", - "reference": "7439a53ae367986e9c22b2ac00f9d7376bb2f8cf", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "phpunit/phpunit": "^9.0" + "source": "https://github.com/Codeception/module-webdriver/tree/3.2.1" }, - "require-dev": { - "codeception/specify": "*", - "consolidation/robo": "^3.0.0-alpha3", - "vlucas/phpdotenv": "^3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\PHPUnit\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - }, - { - "name": "Naktibalda" - } - ], - "description": "PHPUnit classes used by Codeception", - "support": { - "issues": "https://github.com/Codeception/phpunit-wrapper/issues", - "source": "https://github.com/Codeception/phpunit-wrapper/tree/9.0.9" - }, - "time": "2022-05-23T06:24:11+00:00" + "time": "2023-02-03T21:46:32+00:00" }, { "name": "codeception/stub", - "version": "4.0.2", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "18a148dacd293fc7b044042f5aa63a82b08bff5d" + "reference": "58751aed08a68ae960a952fd3fe74ee9a56cdb1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/18a148dacd293fc7b044042f5aa63a82b08bff5d", - "reference": "18a148dacd293fc7b044042f5aa63a82b08bff5d", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/58751aed08a68ae960a952fd3fe74ee9a56cdb1b", + "reference": "58751aed08a68ae960a952fd3fe74ee9a56cdb1b", "shasum": "" }, "require": { "php": "^7.4 | ^8.0", "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | 10.0.x-dev" }, + "conflict": { + "codeception/codeception": "<5.0.6" + }, "require-dev": { "consolidation/robo": "^3.0" }, @@ -9767,9 +10112,9 @@ "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", "support": { "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/4.0.2" + "source": "https://github.com/Codeception/Stub/tree/4.1.0" }, - "time": "2022-01-31T19:25:15+00:00" + "time": "2022-12-27T18:41:43+00:00" }, { "name": "csharpru/vault-php", @@ -9962,103 +10307,78 @@ "time": "2022-09-13T17:27:26+00:00" }, { - "name": "doctrine/annotations", - "version": "1.13.2", + "name": "doctrine/deprecations", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" }, - "time": "2021-08-05T19:00:23+00:00" + "time": "2023-06-03T09:27:29+00:00" }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -10085,7 +10405,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -10101,83 +10421,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" - }, - { - "name": "doctrine/lexer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2022-02-28T11:07:21+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -10268,185 +10512,26 @@ ], "time": "2022-03-18T17:20:59+00:00" }, - { - "name": "jms/metadata", - "version": "2.6.1", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "c3a3214354b5a765a19875f7b7c5ebcd94e462e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/c3a3214354b5a765a19875f7b7c5ebcd94e462e5", - "reference": "c3a3214354b5a765a19875f7b7c5ebcd94e462e5", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "require-dev": { - "doctrine/cache": "^1.0", - "doctrine/coding-standard": "^8.0", - "mikey179/vfsstream": "^1.6.7", - "phpunit/phpunit": "^8.5|^9.0", - "psr/container": "^1.0", - "symfony/cache": "^3.1|^4.0|^5.0", - "symfony/dependency-injection": "^3.1|^4.0|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Metadata\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "Class/method/property metadata management in PHP", - "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" - ], - "support": { - "issues": "https://github.com/schmittjoh/metadata/issues", - "source": "https://github.com/schmittjoh/metadata/tree/2.6.1" - }, - "time": "2021-11-22T12:27:42+00:00" - }, - { - "name": "jms/serializer", - "version": "3.17.1", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/serializer.git", - "reference": "190f64b051795d447ec755acbfdb1bff330a6707" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/190f64b051795d447ec755acbfdb1bff330a6707", - "reference": "190f64b051795d447ec755acbfdb1bff330a6707", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.13", - "doctrine/instantiator": "^1.0.3", - "doctrine/lexer": "^1.1", - "jms/metadata": "^2.6", - "php": "^7.2||^8.0", - "phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0" - }, - "require-dev": { - "doctrine/coding-standard": "^8.1", - "doctrine/orm": "~2.1", - "doctrine/persistence": "^1.3.3|^2.0|^3.0", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "ocramius/proxy-manager": "^1.0|^2.0", - "phpbench/phpbench": "^1.0", - "phpstan/phpstan": "^1.0.2", - "phpunit/phpunit": "^8.5.21||^9.0", - "psr/container": "^1.0", - "symfony/dependency-injection": "^3.0|^4.0|^5.0|^6.0", - "symfony/expression-language": "^3.2|^4.0|^5.0|^6.0", - "symfony/filesystem": "^3.0|^4.0|^5.0|^6.0", - "symfony/form": "^3.0|^4.0|^5.0|^6.0", - "symfony/translation": "^3.0|^4.0|^5.0|^6.0", - "symfony/validator": "^3.1.9|^4.0|^5.0|^6.0", - "symfony/yaml": "^3.3|^4.0|^5.0|^6.0", - "twig/twig": "~1.34|~2.4|^3.0" - }, - "suggest": { - "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", - "symfony/cache": "Required if you like to use cache functionality.", - "symfony/yaml": "Required if you'd like to use the YAML metadata format." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "JMS\\Serializer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", - "homepage": "http://jmsyst.com/libs/serializer", - "keywords": [ - "deserialization", - "jaxb", - "json", - "serialization", - "xml" - ], - "support": { - "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.17.1" - }, - "funding": [ - { - "url": "https://github.com/goetas", - "type": "github" - } - ], - "time": "2021-12-28T20:59:55+00:00" - }, { "name": "laminas/laminas-diactoros", - "version": "2.11.2", + "version": "2.25.2", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "78846cbce0550ec174508a646f46fd6dee76099b" + "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/78846cbce0550ec174508a646f46fd6dee76099b", - "reference": "78846cbce0550ec174508a646f46fd6dee76099b", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", + "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", "shasum": "" }, "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.1" }, "conflict": { - "phpspec/prophecy": "<1.9.0", "zendframework/zend-diactoros": "*" }, "provide": { @@ -10458,13 +10543,12 @@ "ext-dom": "*", "ext-gd": "*", "ext-libxml": "*", - "http-interop/http-factory-tests": "^0.8.0", - "laminas/laminas-coding-standard": "~1.0.0", - "php-http/psr7-integration-tests": "^1.1", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.1", - "psalm/plugin-phpunit": "^0.14.0", - "vimeo/psalm": "^4.3" + "http-interop/http-factory-tests": "^0.9.0", + "laminas/laminas-coding-standard": "^2.5", + "php-http/psr7-integration-tests": "^1.2", + "phpunit/phpunit": "^9.5.28", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.6" }, "type": "library", "extra": { @@ -10523,7 +10607,7 @@ "type": "community_bridge" } ], - "time": "2022-06-29T14:15:02+00:00" + "time": "2023-04-17T15:44:17+00:00" }, { "name": "lusitanian/oauth", @@ -10598,26 +10682,26 @@ }, { "name": "magento/magento-coding-standard", - "version": "27", + "version": "31", "source": { "type": "git", "url": "https://github.com/magento/magento-coding-standard.git", - "reference": "097bda3e015f35dc7c2efc0b8c7a7d8dfc158a63" + "reference": "1172711ea1947d0258adae8d8e0a72669f1c2d99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/097bda3e015f35dc7c2efc0b8c7a7d8dfc158a63", - "reference": "097bda3e015f35dc7c2efc0b8c7a7d8dfc158a63", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/1172711ea1947d0258adae8d8e0a72669f1c2d99", + "reference": "1172711ea1947d0258adae8d8e0a72669f1c2d99", "shasum": "" }, "require": { "ext-dom": "*", "ext-simplexml": "*", - "php": "^8.1||^8.2", + "php": ">=7.4", "phpcompatibility/php-compatibility": "^9.3", - "rector/rector": "^0.13.0", + "rector/rector": "^0.15.10", "squizlabs/php_codesniffer": "^3.6.1", - "webonyx/graphql-php": "^14.9" + "webonyx/graphql-php": "^15.0" }, "require-dev": { "phpunit/phpunit": "^9.5.8" @@ -10640,34 +10724,35 @@ "description": "A set of Magento specific PHP CodeSniffer rules.", "support": { "issues": "https://github.com/magento/magento-coding-standard/issues", - "source": "https://github.com/magento/magento-coding-standard/tree/v27" + "source": "https://github.com/magento/magento-coding-standard/tree/v31" }, - "time": "2022-10-17T15:19:28+00:00" + "time": "2023-02-01T15:38:47+00:00" }, { "name": "magento/magento2-functional-testing-framework", - "version": "4.0.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "daa28ec4aceec147479f8bf1f474873bbd890050" + "reference": "e1af7cfaacff59f1699b1823090abc0995291935" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/daa28ec4aceec147479f8bf1f474873bbd890050", - "reference": "daa28ec4aceec147479f8bf1f474873bbd890050", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/e1af7cfaacff59f1699b1823090abc0995291935", + "reference": "e1af7cfaacff59f1699b1823090abc0995291935", "shasum": "" }, "require": { - "allure-framework/allure-codeception": "^1.4", + "allure-framework/allure-codeception": "^2.1", "allure-framework/allure-phpunit": "^2", "aws/aws-sdk-php": "^3.132", - "codeception/codeception": "^4.1", - "codeception/module-asserts": "^1.1", - "codeception/module-sequence": "^1.0", - "codeception/module-webdriver": "^1.0", + "codeception/codeception": "^5.0", + "codeception/module-asserts": "^3.0", + "codeception/module-sequence": "^3.0", + "codeception/module-webdriver": "^3.0", "composer/composer": "^1.9 || ^2.0, !=2.2.16", "csharpru/vault-php": "^4.2.1", + "doctrine/annotations": "^1.13", "ext-curl": "*", "ext-dom": "*", "ext-iconv": "*", @@ -10679,8 +10764,8 @@ "monolog/monolog": "^2.3", "mustache/mustache": "~2.5", "nikic/php-parser": "^4.4", - "php": ">=8.0", - "php-webdriver/webdriver": "^1.9.0", + "php": ">=8.1", + "php-webdriver/webdriver": "^1.9.0 <1.14.0", "spomky-labs/otphp": "^10.0", "symfony/console": "^4.4||^5.4", "symfony/dotenv": "^5.3", @@ -10696,7 +10781,7 @@ "codacy/coverage": "^1.4", "php-coveralls/php-coveralls": "^1.0||^2.2", "phpmd/phpmd": "^2.8.0", - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "<=9.5.20", "sebastian/phpcpd": "~6.0.0", "squizlabs/php_codesniffer": "~3.6.0" }, @@ -10734,22 +10819,22 @@ ], "support": { "issues": "https://github.com/magento/magento2-functional-testing-framework/issues", - "source": "https://github.com/magento/magento2-functional-testing-framework/tree/4.0.1" + "source": "https://github.com/magento/magento2-functional-testing-framework/tree/4.3.2" }, - "time": "2023-01-05T22:05:27+00:00" + "time": "2023-06-19T14:27:26+00:00" }, { "name": "mustache/mustache", - "version": "v2.14.1", + "version": "v2.14.2", "source": { "type": "git", "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "579ffa5c96e1d292c060b3dd62811ff01ad8c24e" + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/579ffa5c96e1d292c060b3dd62811ff01ad8c24e", - "reference": "579ffa5c96e1d292c060b3dd62811ff01ad8c24e", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", "shasum": "" }, "require": { @@ -10784,22 +10869,22 @@ ], "support": { "issues": "https://github.com/bobthecow/mustache.php/issues", - "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.1" + "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" }, - "time": "2022-01-21T06:08:36+00:00" + "time": "2022-08-23T13:07:01+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -10837,7 +10922,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -10845,7 +10930,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "pdepend/pdepend", @@ -11070,16 +11155,16 @@ }, { "name": "php-webdriver/webdriver", - "version": "1.12.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "b27ddf458d273c7d4602106fcaf978aa0b7fe15a" + "reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/b27ddf458d273c7d4602106fcaf978aa0b7fe15a", - "reference": "b27ddf458d273c7d4602106fcaf978aa0b7fe15a", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/6dfe5f814b796c1b5748850aa19f781b9274c36c", + "reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c", "shasum": "" }, "require": { @@ -11129,9 +11214,9 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.12.1" + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.13.1" }, - "time": "2022-05-03T12:16:34+00:00" + "time": "2022-10-11T11:49:44+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -11307,25 +11392,33 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.1", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b2fe4d22a5426f38e014855322200b97b5362c0d", + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", - "psalm/phar": "^4.8" + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, "type": "library", "extra": { @@ -11351,9 +11444,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.2" }, - "time": "2022-03-15T21:29:03+00:00" + "time": "2023-05-30T18:13:47+00:00" }, { "name": "phpmd/phpmd", @@ -11440,27 +11533,28 @@ }, { "name": "phpspec/prophecy", - "version": "v1.15.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/15873c65b207b07765dbc3c95d20fdf4a320cbe2", + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.*", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", @@ -11501,31 +11595,34 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.17.0" }, - "time": "2021-12-08T12:19:24+00:00" + "time": "2023-02-02T15:41:36+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.5.1", + "version": "1.22.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "981cc368a216c988e862a75e526b6076987d1b50" + "reference": "ec58baf7b3c7f1c81b3b00617c953249fb8cf30c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", - "reference": "981cc368a216c988e862a75e526b6076987d1b50", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/ec58baf7b3c7f1c81b3b00617c953249fb8cf30c", + "reference": "ec58baf7b3c7f1c81b3b00617c953249fb8cf30c", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" @@ -11545,22 +11642,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.22.0" }, - "time": "2022-05-05T11:32:40+00:00" + "time": "2023-06-01T12:35:21+00:00" }, { "name": "phpstan/phpstan", - "version": "1.7.10", + "version": "1.9.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "25e069474cf00215b0f64c60a26230908ef3eefa" + "reference": "e5fcc96289cf737304286a9b505fbed091f02e58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/25e069474cf00215b0f64c60a26230908ef3eefa", - "reference": "25e069474cf00215b0f64c60a26230908ef3eefa", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5fcc96289cf737304286a9b505fbed091f02e58", + "reference": "e5fcc96289cf737304286a9b505fbed091f02e58", "shasum": "" }, "require": { @@ -11584,9 +11681,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.7.10" + "source": "https://github.com/phpstan/phpstan/tree/1.9.14" }, "funding": [ { @@ -11597,36 +11698,32 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-06-03T14:12:23+00:00" + "time": "2023-01-19T10:47:09+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -11641,8 +11738,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -11675,7 +11772,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -11683,7 +11780,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11928,16 +12025,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.20", + "version": "9.5.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + "reference": "e329ac6e8744f461518272612a479fde958752fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e329ac6e8744f461518272612a479fde958752fe", + "reference": "e329ac6e8744f461518272612a479fde958752fe", "shasum": "" }, "require": { @@ -11971,7 +12068,6 @@ "sebastian/version": "^3.0.2" }, "require-dev": { - "ext-pdo": "*", "phpspec/prophecy-phpunit": "^2.0.1" }, "suggest": { @@ -12015,7 +12111,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.22" }, "funding": [ { @@ -12027,34 +12123,57 @@ "type": "github" } ], - "time": "2022-04-01T12:37:26+00:00" + "time": "2022-08-20T08:25:46+00:00" }, { - "name": "psr/cache", - "version": "3.0.0", + "name": "psy/psysh", + "version": "v0.11.18", "source": { "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", + "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", "shasum": "" }, "require": { - "php": ">=8.0.0" + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^4.0 || ^3.1", + "php": "^8.0 || ^7.0.8", + "symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", + "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." }, + "bin": [ + "bin/psysh" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "0.11.x-dev" } }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Psr\\Cache\\": "src/" + "Psy\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -12063,48 +12182,48 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" } ], - "description": "Common interface for caching libraries", + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", "keywords": [ - "cache", - "psr", - "psr-6" + "REPL", + "console", + "interactive", + "shell" ], "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.11.18" }, - "time": "2021-02-03T23:26:27+00:00" + "time": "2023-05-23T02:31:11+00:00" }, { "name": "rector/rector", - "version": "0.13.4", + "version": "0.15.11", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "60b4f87a94e7ed17d4494982ba0cfb5a3f2845fd" + "reference": "0034e743daf120f70359b9600a0946a17e3a6364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/60b4f87a94e7ed17d4494982ba0cfb5a3f2845fd", - "reference": "60b4f87a94e7ed17d4494982ba0cfb5a3f2845fd", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/0034e743daf120f70359b9600a0946a17e3a6364", + "reference": "0034e743daf120f70359b9600a0946a17e3a6364", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.7.10" + "phpstan/phpstan": "^1.9.14" }, "conflict": { - "phpstan/phpdoc-parser": "<1.2", - "rector/rector-cakephp": "*", "rector/rector-doctrine": "*", - "rector/rector-laravel": "*", - "rector/rector-nette": "*", - "rector/rector-phpoffice": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-php-parser": "*", "rector/rector-phpunit": "*", - "rector/rector-prefixed": "*", "rector/rector-symfony": "*" }, "bin": [ @@ -12113,7 +12232,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "0.13-dev" + "dev-main": "0.15-dev" } }, "autoload": { @@ -12128,7 +12247,7 @@ "description": "Instant Upgrade and Automated Refactoring of any PHP code", "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/0.13.4" + "source": "https://github.com/rectorphp/rector/tree/0.15.11" }, "funding": [ { @@ -12136,7 +12255,7 @@ "type": "github" } ], - "time": "2022-06-04T08:19:56+00:00" + "time": "2023-02-02T16:53:15+00:00" }, { "name": "sebastian/cli-parser", @@ -12438,16 +12557,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -12492,7 +12611,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -12500,20 +12619,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -12555,7 +12674,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -12563,20 +12682,20 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -12632,7 +12751,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -12640,7 +12759,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -12934,20 +13053,21 @@ "type": "github" } ], + "abandoned": true, "time": "2020-12-07T05:39:23+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -12986,10 +13106,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -12997,7 +13117,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -13056,16 +13176,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -13077,7 +13197,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -13100,7 +13220,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -13108,7 +13228,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -13240,16 +13360,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", "shasum": "" }, "require": { @@ -13292,20 +13412,20 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2022-06-18T07:21:10+00:00" }, { "name": "symfony/dotenv", - "version": "v5.4.5", + "version": "v5.4.22", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "83a2310904a4f5d4f42526227b5a578ac82232a9" + "reference": "77b7660bfcb85e8f28287d557d7af0046bcd2ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/83a2310904a4f5d4f42526227b5a578ac82232a9", - "reference": "83a2310904a4f5d4f42526227b5a578ac82232a9", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/77b7660bfcb85e8f28287d557d7af0046bcd2ca3", + "reference": "77b7660bfcb85e8f28287d557d7af0046bcd2ca3", "shasum": "" }, "require": { @@ -13347,7 +13467,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v5.4.5" + "source": "https://github.com/symfony/dotenv/tree/v5.4.22" }, "funding": [ { @@ -13363,20 +13483,20 @@ "type": "tidelift" } ], - "time": "2022-02-15T17:04:12+00:00" + "time": "2023-03-09T20:36:58+00:00" }, { "name": "symfony/mime", - "version": "v5.4.19", + "version": "v5.4.23", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb" + "reference": "ae0a1032a450a3abf305ee44fc55ed423fbf16e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a858429a9c704edc53fe057228cf9ca282ba48eb", - "reference": "a858429a9c704edc53fe057228cf9ca282ba48eb", + "url": "https://api.github.com/repos/symfony/mime/zipball/ae0a1032a450a3abf305ee44fc55ed423fbf16e3", + "reference": "ae0a1032a450a3abf305ee44fc55ed423fbf16e3", "shasum": "" }, "require": { @@ -13431,7 +13551,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.19" + "source": "https://github.com/symfony/mime/tree/v5.4.23" }, "funding": [ { @@ -13447,27 +13567,25 @@ "type": "tidelift" } ], - "time": "2023-01-09T05:43:46+00:00" + "time": "2023-04-19T09:49:13+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.4.3", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8" + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cc1147cb11af1b43f503ac18f31aa3bec213aba8", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -13500,7 +13618,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.3" + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" }, "funding": [ { @@ -13516,7 +13634,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-05-12T14:21:09+00:00" }, { "name": "symfony/stopwatch", @@ -13582,31 +13700,27 @@ }, { "name": "symfony/yaml", - "version": "v5.3.14", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c441e9d2e340642ac8b951b753dea962d55b669d" + "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c441e9d2e340642ac8b951b753dea962d55b669d", - "reference": "c441e9d2e340642ac8b951b753dea962d55b669d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a9a8337aa641ef2aa39c3e028f9107ec391e5927", + "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-ctype": "~1.8" + "php": ">=8.1", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<4.4" + "symfony/console": "<5.4" }, "require-dev": { - "symfony/console": "^4.4|^5.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "symfony/console": "^5.4|^6.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -13637,7 +13751,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.3.14" + "source": "https://github.com/symfony/yaml/tree/v6.3.0" }, "funding": [ { @@ -13653,43 +13767,50 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:05:39+00:00" + "time": "2023-04-28T13:28:14+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.3", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" + "thecodingmachine/phpstan-strict-rules": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.1-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { "files": [ "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", "deprecated/mssql.php", "deprecated/stats.php", + "deprecated/strings.php", "lib/special_cases.php", + "deprecated/mysqli.php", "generated/apache.php", "generated/apcu.php", "generated/array.php", @@ -13710,6 +13831,7 @@ "generated/fpm.php", "generated/ftp.php", "generated/funchand.php", + "generated/gettext.php", "generated/gmp.php", "generated/gnupg.php", "generated/hash.php", @@ -13719,7 +13841,6 @@ "generated/image.php", "generated/imap.php", "generated/info.php", - "generated/ingres-ii.php", "generated/inotify.php", "generated/json.php", "generated/ldap.php", @@ -13728,20 +13849,14 @@ "generated/mailparse.php", "generated/mbstring.php", "generated/misc.php", - "generated/msql.php", "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", "generated/network.php", "generated/oci8.php", "generated/opcache.php", "generated/openssl.php", "generated/outcontrol.php", - "generated/password.php", "generated/pcntl.php", "generated/pcre.php", - "generated/pdf.php", "generated/pgsql.php", "generated/posix.php", "generated/ps.php", @@ -13752,7 +13867,6 @@ "generated/sem.php", "generated/session.php", "generated/shmop.php", - "generated/simplexml.php", "generated/sockets.php", "generated/sodium.php", "generated/solr.php", @@ -13775,13 +13889,13 @@ "generated/zip.php", "generated/zlib.php" ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -13790,9 +13904,9 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" }, - "time": "2020-10-28T17:51:34+00:00" + "time": "2023-04-05T11:54:14+00:00" }, { "name": "theseer/tokenizer", @@ -13888,10 +14002,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "magento/composer": 10, - "magento/magento-composer-installer": 10 - }, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/dev/tests/acceptance/.gitignore b/dev/tests/acceptance/.gitignore index 88662b9b4ffbf..aba5de1274b04 100644 --- a/dev/tests/acceptance/.gitignore +++ b/dev/tests/acceptance/.gitignore @@ -8,4 +8,5 @@ tests/functional/Magento/_generated vendor/* mftf.log /.credentials.example +/.credentials /utils/ diff --git a/dev/tests/acceptance/tests/_data/transparency_index.gif b/dev/tests/acceptance/tests/_data/transparency_index.gif new file mode 100644 index 0000000000000..a8085d92a46be Binary files /dev/null and b/dev/tests/acceptance/tests/_data/transparency_index.gif differ 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 e44819c597c33..b940be46d3abc 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -15,9 +15,11 @@ use Magento\TestFramework\App\ApiMutableScopeConfig; use Magento\TestFramework\Config\Model\ConfigStorage; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) */ class ApiConfigFixture extends ConfigFixture { @@ -28,6 +30,19 @@ class ApiConfigFixture extends ConfigFixture */ private $valuesToDeleteFromDatabase = []; + /** + * Put Poison Pill + * + * @return void + * @throws \Exception + */ + private function putPill(): void + { + Bootstrap::getObjectManager() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class) + ->put(); + } + /** * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) @@ -46,6 +61,20 @@ protected function setStoreConfigValue(array $matches, $configPathAndValue): voi parent::setStoreConfigValue($matches, $configPathAndValue); } + /** + * @inheritDoc + */ + protected function _assignConfigData(TestCase $test) + { + parent::_assignConfigData($test); + $needUpdates = !empty($this->globalConfigValues) + || !empty($this->storeConfigValues) + || !empty($this->websiteConfigValues); + if ($needUpdates) { + $this->putPill(); + } + } + /** * @inheritdoc */ @@ -88,6 +117,9 @@ protected function setWebsiteConfigValue(array $matches, $configPathAndValue): v */ protected function _restoreConfigData() { + $needUpdates = !empty($this->globalConfigValues) + || !empty($this->storeConfigValues) + || !empty($this->websiteConfigValues); /** @var ConfigResource $configResource */ $configResource = Bootstrap::getObjectManager()->get(ConfigResource::class); /* Restore global values */ @@ -135,6 +167,9 @@ protected function _restoreConfigData() } } $this->websiteConfigValues = []; + if ($needUpdates) { + $this->putPill(); + } } /** diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php index 0c53b5eb464b9..a2e7fcb95a430 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php @@ -101,28 +101,31 @@ public function get(string $query, array $variables = [], string $operationName } /** - * Process response from GraphQl server + * Process response from GraphQL server. * * @param string $response + * @param array $responseHeaders + * @param array $responseCookies * @return mixed * @throws \Exception */ - private function processResponse(string $response) + private function processResponse(string $response, array $responseHeaders = [], array $responseCookies = []) { - $responseArray = $this->json->jsonDecode($response); - + $responseArray = null; + try { + $responseArray = $this->json->jsonDecode($response); + } catch (\Exception $exception) { + // Note: We don't care about this exception because we have error checking bellow if it fails to decode. + } if (!is_array($responseArray)) { //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unknown GraphQL response body: ' . $response); } - - $this->processErrors($responseArray); - + $this->processErrors($responseArray, $responseHeaders, $responseCookies); if (!isset($responseArray['data'])) { //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unknown GraphQL response body: ' . $response); } - return $responseArray['data']; } @@ -153,9 +156,9 @@ public function getWithResponseHeaders( array_filter($requestArray); $response = $this->curlClient->getWithFullResponse($url, $requestArray, $headers, $flushCookies); - $responseBody = $this->processResponse($response['body']); $responseHeaders = !empty($response['header']) ? $this->processResponseHeaders($response['header']) : []; $responseCookies = !empty($response['header']) ? $this->processResponseCookies($response['header']) : []; + $responseBody = $this->processResponse($response['body'], $responseHeaders, $responseCookies); return ['headers' => $responseHeaders, 'body' => $responseBody, 'cookies' => $responseCookies]; } @@ -188,20 +191,23 @@ public function postWithResponseHeaders( $postData = $this->json->jsonEncode($requestArray); $response = $this->curlClient->postWithFullResponse($url, $postData, $headers, $flushCookies); - $responseBody = $this->processResponse($response['body']); $responseHeaders = !empty($response['header']) ? $this->processResponseHeaders($response['header']) : []; $responseCookies = !empty($response['header']) ? $this->processResponseCookies($response['header']) : []; + $responseBody = $this->processResponse($response['body'], $responseHeaders, $responseCookies); return ['headers' => $responseHeaders, 'body' => $responseBody, 'cookies' => $responseCookies]; } /** - * Process errors + * Process errors. * * @param array $responseBodyArray - * @throws \Exception + * @param array $responseHeaders + * @param array $responseCookies + * @return void + * @throws ResponseContainsErrorsException */ - private function processErrors($responseBodyArray) + private function processErrors($responseBodyArray, array $responseHeaders = [], array $responseCookies = []) { if (isset($responseBodyArray['errors'])) { $errorMessage = ''; @@ -220,8 +226,12 @@ private function processErrors($responseBodyArray) } throw new ResponseContainsErrorsException( - 'GraphQL response contains errors: ' . $errorMessage, - $responseBodyArray + 'GraphQL response contains errors: ' . $errorMessage . "\n" . var_export($responseBodyArray, true), + $responseBodyArray, + null, + 0, + $responseHeaders, + $responseCookies ); } //phpcs:ignore Magento2.Exceptions.DirectThrow diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResolverCacheAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResolverCacheAbstract.php new file mode 100644 index 0000000000000..21d20a9471b54 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResolverCacheAbstract.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\TestCase\GraphQl; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\App\Area; +use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; +use Magento\Framework\App\ObjectManager\ConfigLoader; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\Model\Query\ContextFactory; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\TestFramework\App\State; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ResolverCacheAbstract extends GraphQlAbstract +{ + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var CacheStateInterface + */ + private $cacheState; + + /** + * @var bool + */ + private $originalCacheStateEnabledStatus; + + /** + * @var string + */ + private $initialAppArea; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + // test has to be executed in graphql area + $configLoader = $this->objectManager->get(ConfigLoader::class); + /** @var State $appArea */ + $appArea = $this->objectManager->get(State::class); + $this->initialAppArea = $appArea->getAreaCode(); + $this->objectManager->configure($configLoader->load(Area::AREA_GRAPHQL)); + $this->mockGuestUserInfoContext(); + + $this->cacheState = $this->objectManager->get(CacheStateInterface::class); + $this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER); + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true); + $this->graphQlResolverCache = $this->objectManager->get(GraphQlResolverCache::class); + + parent::setUp(); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + // clean graphql resolver cache and reset to original enablement status + $this->graphQlResolverCache->clean(); + $this->cacheState->setEnabled( + GraphQlResolverCache::TYPE_IDENTIFIER, + $this->originalCacheStateEnabledStatus + ); + + /** @var ConfigLoader $configLoader */ + $configLoader = $this->objectManager->get(ConfigLoader::class); + $this->objectManager->configure($configLoader->load($this->initialAppArea)); + $this->objectManager->removeSharedInstance(ContextFactory::class); + $this->objectManager->removeSharedInstance(\Magento\GraphQl\Model\Query\Context::class); + $this->objectManager->removeSharedInstance(\Magento\GraphQl\Model\Query\ContextInterface::class); + + parent::tearDown(); + } + + /** + * Initialize test-scoped user context with $customer + * + * @param CustomerInterface $customer + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + protected function mockCustomerUserInfoContext(CustomerInterface $customer) + { + $userContextMock = $this->getMockBuilder(UserContextInterface::class) + ->onlyMethods(['getUserId', 'getUserType']) + ->disableOriginalConstructor() + ->getMock(); + $userContextMock->expects($this->any()) + ->method('getUserId') + ->willReturn($customer->getId()); + $userContextMock->expects($this->any()) + ->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); + + /** @var ContextFactory $contextFactory */ + $contextFactory = $this->objectManager->get(ContextFactory::class); + $contextFactory->create($userContextMock); + } + + /** + * Reset test-scoped user context to guest. + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function mockGuestUserInfoContext() + { + $userContextMock = $this->getMockBuilder(UserContextInterface::class) + ->onlyMethods(['getUserId', 'getUserType']) + ->disableOriginalConstructor() + ->getMock(); + $userContextMock->expects($this->any()) + ->method('getUserId') + ->willReturn(0); + $userContextMock->expects($this->any()) + ->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_GUEST); + // test has to be executed in graphql area + $configLoader = $this->objectManager->get(ConfigLoader::class); + $this->objectManager->configure($configLoader->load(Area::AREA_GRAPHQL)); + /** @var ContextFactory $contextFactory */ + $contextFactory = $this->objectManager->get(ContextFactory::class); + $contextFactory->create($userContextMock); + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php index 568de57543d84..d0b44ffdd741e 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php @@ -8,7 +8,7 @@ namespace Magento\TestFramework\TestCase\GraphQl; /** - * Response contains errors exception + * Exception thrown when GraphQL response contains errors. */ class ResponseContainsErrorsException extends \Exception { @@ -17,16 +17,36 @@ class ResponseContainsErrorsException extends \Exception */ private $responseData; + /** + * @var array + */ + private $responseHeaders; + + /** + * @var array + */ + private $responseCookies; + /** * @param string $message * @param array $responseData * @param \Exception|null $cause * @param int $code + * @param array $responseHeaders + * @param array $responseCookies */ - public function __construct(string $message, array $responseData, \Exception $cause = null, int $code = 0) - { + public function __construct( + string $message, + array $responseData, + \Exception $cause = null, + int $code = 0, + array $responseHeaders = [], + array $responseCookies = [] + ) { parent::__construct($message, $code, $cause); $this->responseData = $responseData; + $this->responseHeaders = $responseHeaders; + $this->responseCookies = $responseCookies; } /** @@ -38,4 +58,24 @@ public function getResponseData(): array { return $this->responseData; } + + /** + * Get response headers + * + * @return array + */ + public function getResponseHeaders(): array + { + return $this->responseHeaders; + } + + /** + * Get response cookies + * + * @return array + */ + public function getResponseCookies(): array + { + return $this->responseCookies; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php index 1616e22a9d61d..27dc9cbb555ae 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/TierPriceStorageTest.php @@ -18,6 +18,10 @@ class TierPriceStorageTest extends WebapiAbstract private const SERVICE_NAME = 'catalogTierPriceStorageV1'; private const SERVICE_VERSION = 'V1'; private const SIMPLE_PRODUCT_SKU = 'simple'; + private const CUSTOMER_ALL_GROUPS_NAME ='ALL GROUPS'; + private const CUSTOMER_GENERAL_GROUP_NAME ='General'; + private const CUSTOMER_NOT_LOGGED_IN_GROUP_NAME ='NOT LOGGED IN'; + private const WRONG_CUSTOMER_GROUP_NAME ='general'; /** * @var \Magento\TestFramework\ObjectManager @@ -89,7 +93,7 @@ public function testUpdate() 'price_type' => TierPriceInterface::PRICE_TYPE_DISCOUNT, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'ALL GROUPS', + 'customer_group' => self::CUSTOMER_ALL_GROUPS_NAME, 'quantity' => 7778 ]; $updatedPrice = [ @@ -97,7 +101,7 @@ public function testUpdate() 'price_type' => TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'not logged in', + 'customer_group' => self::CUSTOMER_NOT_LOGGED_IN_GROUP_NAME, 'quantity' => $tierPrice->getQty() ]; $response = $this->_webApiCall($serviceInfo, ['prices' => [$updatedPrice, $newPrice]]); @@ -178,7 +182,7 @@ public function testReplaceWithoutErrorMessage() 'price_type' => TierPriceInterface::PRICE_TYPE_DISCOUNT, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'general', + 'customer_group' => self::CUSTOMER_GENERAL_GROUP_NAME, 'quantity' => 7778 ], [ @@ -186,7 +190,7 @@ public function testReplaceWithoutErrorMessage() 'price_type' => TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'not logged in', + 'customer_group' => self::CUSTOMER_NOT_LOGGED_IN_GROUP_NAME, 'quantity' => 33 ] ]; @@ -222,7 +226,7 @@ public function testReplaceWithErrorMessage() 'price_type' => TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'general', + 'customer_group' => self::WRONG_CUSTOMER_GROUP_NAME, 'quantity' => 2 ], [ @@ -230,7 +234,7 @@ public function testReplaceWithErrorMessage() 'price_type' => TierPriceInterface::PRICE_TYPE_FIXED, 'website_id' => 0, 'sku' => self::SIMPLE_PRODUCT_SKU, - 'customer_group' => 'general', + 'customer_group' => self::WRONG_CUSTOMER_GROUP_NAME, 'quantity' => 2 ] ]; @@ -264,8 +268,8 @@ public function testDelete() ? TierPriceInterface::PRICE_TYPE_DISCOUNT : TierPriceInterface::PRICE_TYPE_FIXED; $customerGroup = $tierPrice->getCustomerGroupId() == \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID - ? 'NOT LOGGED IN' - : 'ALL GROUPS'; + ? self::CUSTOMER_NOT_LOGGED_IN_GROUP_NAME + : self::CUSTOMER_ALL_GROUPS_NAME; $pricesToDelete[] = [ 'price' => $tierPriceValue, 'price_type' => $priceType, diff --git a/dev/tests/api-functional/testsuite/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryTest.php index 3893c5d196b60..aaaa52c5d4553 100644 --- a/dev/tests/api-functional/testsuite/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryTest.php @@ -65,9 +65,10 @@ public function testGetListReturnsEmptyListIfCheckoutAgreementsAreDisabledOnFron */ public function testGetListReturnsTheListOfActiveCheckoutAgreements() { + $this->markTestSkipped('This test relies on system configuration state.'); // checkout/options/enable_agreements must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + $agreementModel = $this->getAgreementByName('Checkout Agreement (active)'); $agreements = $this->_webApiCall($this->listServiceInfo, []); diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php index 63f6814897863..0bac15256d355 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php @@ -8,10 +8,13 @@ use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Model\AccountManagement; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Newsletter\Model\Subscriber; use Magento\Security\Model\Config; +use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -23,15 +26,20 @@ */ class AccountManagementTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'customerAccountManagementV1'; - const RESOURCE_PATH = '/V1/customers'; + public const SERVICE_VERSION = 'V1'; + public const SERVICE_NAME = 'customerAccountManagementV1'; + public const RESOURCE_PATH = '/V1/customers'; /** * Sample values for testing */ - const ATTRIBUTE_CODE = 'attribute_code'; - const ATTRIBUTE_VALUE = 'attribute_value'; + public const ATTRIBUTE_CODE = 'attribute_code'; + public const ATTRIBUTE_VALUE = 'attribute_value'; + + /** + * @var ObjectManager + */ + private $objectManager; /** * @var AccountManagementInterface @@ -86,6 +94,8 @@ class AccountManagementTest extends WebapiAbstract */ protected function setUp(): void { + $this->objectManager = Bootstrap::getObjectManager(); + $this->accountManagement = Bootstrap::getObjectManager()->get( \Magento\Customer\Api\AccountManagementInterface::class ); @@ -645,6 +655,7 @@ public function testIsReadonly() public function testEmailAvailable() { + $config = $this->objectManager->get(ScopeConfigInterface::class); $customerData = $this->_createCustomer(); $serviceInfo = [ @@ -662,7 +673,18 @@ public function testEmailAvailable() 'customerEmail' => $customerData[Customer::EMAIL], 'websiteId' => $customerData[Customer::WEBSITE_ID], ]; - $this->assertFalse($this->_webApiCall($serviceInfo, $requestData)); + + $emailSetting = $config->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + $customerData[Customer::WEBSITE_ID] + ); + + if (!$emailSetting) { + $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); + } else { + $this->assertFalse($this->_webApiCall($serviceInfo, $requestData)); + } } public function testEmailAvailableInvalidEmail() diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php index e16917f7e454a..88a79f77ddbc0 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php @@ -18,9 +18,9 @@ */ class AddressMetadataTest extends WebapiAbstract { - const SERVICE_NAME = "customerAddressMetadataV1"; - const SERVICE_VERSION = "V1"; - const RESOURCE_PATH = "/V1/attributeMetadata/customerAddress"; + private const SERVICE_NAME = "customerAddressMetadataV1"; + private const SERVICE_VERSION = "V1"; + private const RESOURCE_PATH = "/V1/attributeMetadata/customerAddress"; /** * @var Config $config @@ -372,22 +372,6 @@ public function checkMultipleAttributesValidationRules($expectedResult, $actualR return [$expectedResult, $actualResultSet]; } - /** - * Remove test attribute - */ - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - /** @var \Magento\Customer\Model\Attribute $attribute */ - $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Attribute::class - ); - foreach (['custom_attribute1', 'custom_attribute2'] as $attributeCode) { - $attribute->loadByCode('customer_address', $attributeCode); - $attribute->delete(); - } - } - /** * Set core config data. * diff --git a/dev/tests/api-functional/testsuite/Magento/Framework/Stdlib/CookieManagerTest.php b/dev/tests/api-functional/testsuite/Magento/Framework/Stdlib/CookieManagerTest.php index da5cec838729d..f8f29764faeb5 100644 --- a/dev/tests/api-functional/testsuite/Magento/Framework/Stdlib/CookieManagerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Framework/Stdlib/CookieManagerTest.php @@ -16,6 +16,9 @@ */ class CookieManagerTest extends \Magento\TestFramework\TestCase\WebapiAbstract { + /** + * @var string + */ private $cookieTesterUrl = 'testmoduleone/CookieTester'; /** @var CurlClientWithCookies */ @@ -144,7 +147,10 @@ public function testDeleteCookie() if (isset($cookie['max-age'])) { $this->assertEquals(0, $cookie['max-age']); } - $this->assertEquals('Thu, 01-Jan-1970 00:00:01 GMT', $cookie['expires']); + $this->assertEquals( + date('D, j-M-o H:i:s T', strtotime('Thu, 01-Jan-1970 00:00:01 GMT')), + date('D, j-M-o H:i:s T', strtotime($cookie['expires'])) + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/CartRepositoryTest.php index d42166fe1d529..4a5260d8e3ab1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/CartRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/CartRepositoryTest.php @@ -9,9 +9,9 @@ class CartRepositoryTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'giftMessageCartRepositoryV1'; - const RESOURCE_PATH = '/V1/carts/'; + public const SERVICE_VERSION = 'V1'; + public const SERVICE_NAME = 'giftMessageCartRepositoryV1'; + public const RESOURCE_PATH = '/V1/carts/'; /** * @var \Magento\TestFramework\ObjectManager @@ -102,9 +102,10 @@ public function testGetForMyCart() */ public function testSave() { + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_order must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_message', 'reserved_order_id'); @@ -155,9 +156,9 @@ public function testSaveForMyCart() ); $token = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_order must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed $serviceInfo = [ 'rest' => [ diff --git a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestCartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestCartRepositoryTest.php index bbb8a18f07c0b..a5579cc5fe45d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestCartRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestCartRepositoryTest.php @@ -9,9 +9,9 @@ class GuestCartRepositoryTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'giftMessageGuestCartRepositoryV1'; - const RESOURCE_PATH = '/V1/guest-carts/'; + public const SERVICE_VERSION = 'V1'; + public const SERVICE_NAME = 'giftMessageGuestCartRepositoryV1'; + public const RESOURCE_PATH = '/V1/guest-carts/'; /** * @var \Magento\TestFramework\ObjectManager @@ -73,9 +73,10 @@ public function testGet() */ public function testSave() { + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_order must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_message', 'reserved_order_id'); diff --git a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestItemRepositoryTest.php index 81a1bee7acf8d..d35a5c91e91b1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestItemRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/GuestItemRepositoryTest.php @@ -9,9 +9,9 @@ class GuestItemRepositoryTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'giftMessageGuestItemRepositoryV1'; - const RESOURCE_PATH = '/V1/guest-carts/'; + public const SERVICE_VERSION = 'V1'; + public const SERVICE_NAME = 'giftMessageGuestItemRepositoryV1'; + public const RESOURCE_PATH = '/V1/guest-carts/'; /** * @var \Magento\TestFramework\ObjectManager @@ -74,12 +74,14 @@ public function testGet() /** * @magentoApiDataFixture Magento/GiftMessage/_files/quote_with_item_message.php + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 */ public function testSave() { + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_items must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_message', 'reserved_order_id'); diff --git a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/ItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/ItemRepositoryTest.php index e8aa8f044c995..a06d03d0d7d9b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/ItemRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GiftMessage/Api/ItemRepositoryTest.php @@ -9,9 +9,9 @@ class ItemRepositoryTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'giftMessageItemRepositoryV1'; - const RESOURCE_PATH = '/V1/carts/'; + public const SERVICE_VERSION = 'V1'; + public const SERVICE_NAME = 'giftMessageItemRepositoryV1'; + public const RESOURCE_PATH = '/V1/carts/'; /** * @var \Magento\TestFramework\ObjectManager @@ -112,9 +112,10 @@ public function testGetForMyCart() */ public function testSave() { + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_items must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_message', 'reserved_order_id'); @@ -166,9 +167,10 @@ public function testSaveForMyCart() ); $token = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $this->markTestSkipped('This test relies on system configuration state.'); // sales/gift_options/allow_items must be set to 1 in system configuration - // @todo remove next statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed - $this->markTestIncomplete('This test relies on system configuration state.'); + // @todo remove above statement when \Magento\TestFramework\TestCase\WebapiAbstract::_updateAppConfig is fixed + /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_message', 'reserved_order_id'); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php index 3207d24583080..cff69caa05a63 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php @@ -212,7 +212,7 @@ public function testBundleProductWithNotVisibleChildren() } $this->assertBundleBaseFields($bundleProduct, $response['products']['items'][0]); - $this->assertBundleProductOptions($bundleProduct, $response['products']['items'][0], false); + $this->assertBundleProductOptions($bundleProduct, $response['products']['items'][0]); $this->assertNotEmpty( $response['products']['items'][0]['items'], "Precondition failed: 'items' must not be empty" @@ -242,9 +242,8 @@ private function assertBundleBaseFields($product, $actualResponse) /** * @param ProductInterface $product * @param array $actualResponse - * @param bool $isChildVisible */ - private function assertBundleProductOptions($product, $actualResponse, $isChildVisible = true) + private function assertBundleProductOptions($product, $actualResponse) { $this->assertNotEmpty( $actualResponse['items'], @@ -284,22 +283,18 @@ private function assertBundleProductOptions($product, $actualResponse, $isChildV ] ); $this->assertEquals( - $isChildVisible ? $childProduct->getName() : null, + $childProduct->getName(), $actualResponse['items'][0]['options'][0]['label'] ); - if ($isChildVisible) { - $this->assertResponseFields( - $actualResponse['items'][0]['options'][0]['product'], - [ - 'id' => $childProduct->getId(), - 'name' => $childProduct->getName(), - 'type_id' => $childProduct->getTypeId(), - 'sku' => $childProduct->getSku() - ] - ); - } else { - $this->assertNull($actualResponse['items'][0]['options'][0]['product']); - } + $this->assertResponseFields( + $actualResponse['items'][0]['options'][0]['product'], + [ + 'id' => $childProduct->getId(), + 'name' => $childProduct->getName(), + 'type_id' => $childProduct->getTypeId(), + 'sku' => $childProduct->getSku() + ] + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php new file mode 100644 index 0000000000000..3ccb3a3489e19 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/GetProductWithCustomAttributesTest.php @@ -0,0 +1,378 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Test\Fixture\Attribute; +use Magento\Catalog\Test\Fixture\MultiselectAttribute; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test products with custom attributes query output + */ +#[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => 'product_custom_attribute', + 'is_comparable' => 1, + 'is_visible_on_front' => 1 + ], + 'varchar_custom_attribute' + ), + DataFixture( + MultiselectAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'product_custom_attribute_multiselect' + ], + 'multiselect_custom_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'label' => 'red', + 'sort_order' => 20 + ], + 'multiselect_custom_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'white', + 'is_default' => true + ], + 'multiselect_custom_attribute_option_2' + ), + DataFixture( + ProductFixture::class, + [ + 'custom_attributes' => [ + [ + 'attribute_code' => '$varchar_custom_attribute.attribute_code$', + 'value' => 'test_value' + ], + [ + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'selected_options' => [ + ['value' => '$multiselect_custom_attribute_option_1.value$'], + ['value' => '$multiselect_custom_attribute_option_2.value$'] + ], + ], + ], + ], + 'product' + ), +] +class GetProductWithCustomAttributesTest extends GraphQlAbstract +{ + /** + * @var AttributeInterface|null + */ + private $varcharCustomAttribute; + + /** + * @var AttributeInterface|null + */ + private $multiselectCustomAttribute; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomAttributeOption1; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomAttributeOption2; + + /** + * @var Product|null + */ + private $product; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->varcharCustomAttribute = DataFixtureStorageManager::getStorage()->get( + 'varchar_custom_attribute' + ); + $this->multiselectCustomAttribute = DataFixtureStorageManager::getStorage()->get( + 'multiselect_custom_attribute' + ); + $this->multiselectCustomAttributeOption1 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_custom_attribute_option_1' + ); + $this->multiselectCustomAttributeOption2 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_custom_attribute_option_2' + ); + + $this->product = DataFixtureStorageManager::getStorage()->get('product'); + } + + public function testGetProductWithCustomAttributes() + { + $productSku = $this->product->getSku(); + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items + { + sku + name + custom_attributesV2 { + items { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + }, + errors { + type + message + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + $this->assertProductCustomAttributesResult($response); + $this->assertEmpty(count($response['products']['items'][0]['custom_attributesV2']['errors'])); + } + + public function testGetNoResultsWhenFilteringByNotExistingSku() + { + $query = <<<QUERY +{ + products(filter: {sku: {eq: "not_existing_sku"}}) + { + items + { + sku + name + custom_attributesV2 { + items { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('items', $response['products'], 'Query result must not contain products'); + $this->assertCount(0, $response['products']['items']); + } + + public function testGetProductCustomAttributesFiltered() + { + $productSku = $this->product->getSku(); + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items + { + sku + name + custom_attributesV2(filters: {is_comparable: true, is_visible_on_front: true}) { + items { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + }, + errors { + type + message + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertEquals( + [ + 'products' => [ + 'items' => [ + 0 => [ + 'sku' => $this->product->getSku(), + 'name' => $this->product->getName(), + 'custom_attributesV2' => [ + 'items' => [ + 0 => [ + 'code' => $this->varcharCustomAttribute->getAttributeCode(), + 'value' => 'test_value' + ] + ], + 'errors' => [] + ] + ] + ] + ] + ], + $response + ); + } + + public function testGetProductCustomAttributesFilteredByNotExistingField() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Field "not_existing_filter" is not defined by type "AttributeFilterInput"'); + $productSku = $this->product->getSku(); + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items + { + sku + name + custom_attributesV2(filters: {not_existing_filter: true}) { + items { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } + } +} +QUERY; + + $this->graphQlQuery($query); + } + + /** + * Finds attribute in query result + * + * @param array $items + * @param string $attribute_code + * @return array + */ + private function getAttributeByCode(array $items, string $attribute_code): array + { + $attribute = array_filter($items, function ($item) use ($attribute_code) { + return $item['code'] == $attribute_code; + }); + + return array_merge(...$attribute); + } + + /** + * @param array $response + */ + private function assertProductCustomAttributesResult(array $response): void + { + $this->assertArrayHasKey('items', $response['products'], 'Query result does not contain products'); + $this->assertArrayHasKey( + 'items', + $response['products']['items'][0]['custom_attributesV2'], + 'Query result does not contain custom attributes' + ); + $this->assertGreaterThanOrEqual(2, count($response['products']['items'][0]['custom_attributesV2']['items'])); + + $this->assertResponseFields( + $response['products']['items'][0], + [ + 'sku' => $this->product->getSku(), + 'name' => $this->product->getName() + ] + ); + + $this->assertResponseFields( + $this->getAttributeByCode( + $response['products']['items'][0]['custom_attributesV2']['items'], + $this->varcharCustomAttribute->getAttributeCode() + ), + [ + 'code' => $this->varcharCustomAttribute->getAttributeCode(), + 'value' => 'test_value' + ] + ); + + $this->assertResponseFields( + $this->getAttributeByCode( + $response['products']['items'][0]['custom_attributesV2']['items'], + $this->multiselectCustomAttribute->getAttributeCode() + ), + [ + 'code' => $this->multiselectCustomAttribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->multiselectCustomAttributeOption2->getLabel(), + 'value' => $this->multiselectCustomAttributeOption2->getValue(), + ], + [ + 'label' => $this->multiselectCustomAttributeOption1->getLabel(), + 'value' => $this->multiselectCustomAttributeOption1->getValue(), + ] + ] + ] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductFragmentTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductFragmentTest.php index 32a2f8f763572..ff932026c5ce2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductFragmentTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductFragmentTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\Catalog; +use Exception; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -16,8 +17,9 @@ class ProductFragmentTest extends GraphQlAbstract { /** * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @throws Exception */ - public function testSimpleProductFragment() + public function testSimpleProductNamedFragment(): void { $sku = 'simple'; $name = 'Simple Product'; @@ -36,9 +38,9 @@ public function testSimpleProductFragment() fragment BasicProductInformation on ProductInterface { sku name - price { - regularPrice { - amount { + price_range{ + minimum_price{ + final_price{ value } } @@ -49,6 +51,42 @@ public function testSimpleProductFragment() $actualProductData = $result['products']['items'][0]; $this->assertNotEmpty($actualProductData); $this->assertEquals($name, $actualProductData['name']); - $this->assertEquals($price, $actualProductData['price']['regularPrice']['amount']['value']); + $this->assertEquals($price, $actualProductData['price_range']['minimum_price']['final_price']['value']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @throws Exception + */ + public function testSimpleProductInlineFragment(): void + { + $sku = 'simple'; + $name = 'Simple Product'; + $price = 10; + + $query = <<<QUERY +query GetProduct { + products(filter: { sku: { eq: "$sku" } }) { + items { + sku + ... on ProductInterface { + name + price_range{ + minimum_price{ + final_price{ + value + } + } + } + } + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + $actualProductData = $result['products']['items'][0]; + $this->assertNotEmpty($actualProductData); + $this->assertEquals($name, $actualProductData['name']); + $this->assertEquals($price, $actualProductData['price_range']['minimum_price']['final_price']['value']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php index 2f050bc55df36..fa1da9ad8b0d7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -37,6 +37,11 @@ */ class ProductPriceTest extends GraphQlAbstract { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** @var ObjectManager $objectManager */ private $objectManager; @@ -368,14 +373,14 @@ public function priceDataProvider() : array "simple1" => [ 0 => [ 'discount' =>['amount_off' => 1, 'percent_off' => 10], - 'final_price' =>['value'=> 9], + 'final_price' =>['value'=> 9 * 2], 'quantity' => 2 ] ], "simple2" => [ 0 => [ 'discount' =>['amount_off' => 2, 'percent_off' => 10], - 'final_price' =>['value'=> 18], + 'final_price' =>['value'=> 18 * 2], 'quantity' => 2 ] ] @@ -414,14 +419,14 @@ public function priceDataProvider() : array "simple1" => [ 0 => [ 'discount' =>['amount_off' => 1, 'percent_off' => 10], - 'final_price' =>['value'=> 9], + 'final_price' =>['value'=> 9 * 2 ], 'quantity' => 2 ] ], "simple2" => [ 0 => [ 'discount' =>['amount_off' => 2, 'percent_off' => 10], - 'final_price' =>['value'=> 18], + 'final_price' =>['value'=> 18 * 2], 'quantity' => 2 ] ] @@ -601,7 +606,7 @@ public function testBundledProductWithSpecialPriceAndTierPrice() 'amount_off' => 1, 'percent_off' => 10 ], - 'final_price' =>['value'=> 9], + 'final_price' =>['value'=> 9 * 2], 'quantity' => 2 ] ] @@ -692,7 +697,7 @@ public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() 'customer_group_id' => Group::CUST_GROUP_ALL, 'percentage_value'=> null, 'qty'=> 2, - 'value'=> 20 + 'value'=> 20, ] ]; foreach ($configurableProductVariants as $configurableProductVariant) { @@ -772,7 +777,7 @@ public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() "value" => round((float) $configurableProductVariants[$key]->getSpecialPrice(), 2) ], "discount" => [ - "amount_off" => ($regularPrice[$key] - $finalPrice[$key]), + "amount_off" => round($regularPrice[$key] - $finalPrice[$key], 2), "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) ] ], @@ -784,7 +789,7 @@ public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() "value" => round((float) $configurableProductVariants[$key]->getSpecialPrice(), 2) ], "discount" => [ - "amount_off" => $regularPrice[$key] - $finalPrice[$key], + "amount_off" => round($regularPrice[$key] - $finalPrice[$key], 2), "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) ] ] @@ -804,7 +809,7 @@ public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() 2 ) ], - 'final_price' =>['value'=> $tierPriceData[0]['value']], + 'final_price' =>['value'=> $tierPriceData[0]['value'] * 2], 'quantity' => 2 ] ] @@ -882,7 +887,7 @@ public function testDownloadableProductWithSpecialPriceAndTierPrices() 'amount_off' => 3, 'percent_off' => 30 ], - 'final_price' =>['value'=> 7], + 'final_price' =>['value'=> 7 * 2], 'quantity' => 2 ] ] @@ -1218,8 +1223,16 @@ private function assertPrices($expectedPrices, $actualPrices, $currency = 'USD') $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']); + $this->assertEqualsWithDelta( + $expected['discount']['amount_off'], + $actual['discount']['amount_off'], + self::EPSILON + ); + $this->assertEqualsWithDelta( + $expected['discount']['percent_off'], + $actual['discount']['percent_off'], + self::EPSILON + ); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 94e40f4132cfd..1439bc3e303ae 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -37,11 +37,11 @@ function ($a) { $booleanAggregation = reset($booleanAggregation); $this->assertEquals('Boolean Attribute', $booleanAggregation['label']); $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); - $this->assertContainsEquals(['label' => '1', 'value' => '1', 'count' => '3'], $booleanAggregation['options']); + $this->assertContainsEquals(['label' => 'Yes', 'value' => '1', 'count' => '3'], $booleanAggregation['options']); $this->assertEquals(2, $booleanAggregation['count']); $this->assertCount(2, $booleanAggregation['options']); - $this->assertContainsEquals(['label' => '0', 'value' => '0', 'count' => '2'], $booleanAggregation['options']); + $this->assertContainsEquals(['label' => 'No', 'value' => '0', 'count' => '2'], $booleanAggregation['options']); } /** 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 09e0256f80f41..8876984aefbbd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -119,7 +119,7 @@ protected function setUp(): void * * @throws \Exception */ - public function testFilterForNonExistingCategory() + public function testFilterForNonExistingCategory(): void { $query = <<<QUERY { @@ -148,7 +148,7 @@ public function testFilterForNonExistingCategory() /** * Verify that filters id and uid can't be used at the same time */ - public function testUidAndIdUsageErrorOnProductFilteringCategory() + public function testUidAndIdUsageErrorOnProductFilteringCategory(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('`category_id` and `category_uid` can\'t be used at the same time'); @@ -164,6 +164,139 @@ public function testUidAndIdUsageErrorOnProductFilteringCategory() $this->graphQlQuery($query); } + /** + * Verify that filters category url path and uid can't be used at the same time + */ + public function testUidAndCategoryUrlPathUsageErrorOnProductFilteringCategory(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`category_uid` and `category_url_path` can\'t be used at the same time'); + $query = <<<QUERY +{ + products(filter: {category_uid: {eq: "OTk5OTk5OTk="}, category_url_path: {eq: "category-1/category-1-2"}}) { + filters { + name + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * Filter by category url path + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterByCategoryUrlPath(): void + { + $categoryUrlPath = 'category-1/category-1-2'; + $query = <<<QUERY +{ + products(filter:{ + category_url_path : {eq:"{$categoryUrlPath}"} + }) { + total_count + items { + name + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(2, $response['products']['total_count']); + /** @var ProductRepositoryInterface $productRepository */ + $product1 = $this->productRepository->get('simple'); + $product2 = $this->productRepository->get('simple-4'); + $filteredProducts = [$product2, $product1]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ + 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + } + + /** + * Filter by multiple categories url paths + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByMultipleCategoriesUrlPaths(): void + { + $categoriesPath = ['category-1/category-1-2','category-1/category-1-1']; + + $query = <<<QUERY +{ + products(filter:{ + category_url_path : {in:["{$categoriesPath[0]}","{$categoriesPath[1]}"]} + }) { + total_count + items { + name + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + /** @var ProductRepositoryInterface $productRepository */ + $product1 = $this->productRepository->get('simple'); + $product2 = $this->productRepository->get('12345'); + $product3 = $this->productRepository->get('simple-4'); + $filteredProducts = [$product3, $product2, $product1]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ + 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + } + + /** + * Filter by wrong category url path + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterByWrongCategoryUrlPath(): void + { + $categoryUrlPath = 'not-a-category url path'; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('No category with the provided `category_url_path` was found'); + + $query = <<<QUERY +{ + products(filter:{ + category_url_path : {eq:"{$categoryUrlPath}"} + }) { + total_count + items { + name + sku + } + } +} +QUERY; + $this->graphQlQuery($query); + } + /** * Verify that layered navigation filters and aggregations are correct for product query * @@ -171,7 +304,7 @@ public function testUidAndIdUsageErrorOnProductFilteringCategory() * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterLn() + public function testFilterLn(): void { $query = <<<QUERY { @@ -238,7 +371,7 @@ public function testFilterLn() * @param array $b * @return int */ - private function compareFilterNames(array $a, array $b) + private function compareFilterNames(array $a, array $b): int { return strcmp($a['name'], $b['name']); } @@ -252,7 +385,7 @@ private function compareFilterNames(array $a, array $b) * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testLayeredNavigationForConfigurableProducts() + public function testLayeredNavigationForConfigurableProducts(): void { $attributeCode = 'test_configurable'; $attribute = $this->eavConfig->getAttribute('catalog_product', $attributeCode); @@ -358,7 +491,7 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsByDropDownCustomAttribute() + public function testFilterProductsByDropDownCustomAttribute(): void { CacheCleaner::clean(['eav']); $attributeCode = 'second_test_configurable'; @@ -470,7 +603,7 @@ public function testFilterProductsByDropDownCustomAttribute() * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsByMultiSelectCustomAttributes() + public function testFilterProductsByMultiSelectCustomAttributes(): void { $attributeCode = 'multiselect_attribute'; $attribute = $this->eavConfig->getAttribute('catalog_product', $attributeCode); @@ -560,7 +693,7 @@ private function getDefaultAttributeOptionValue(string $attributeCode): string * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testSearchAndFilterByCustomAttribute() + public function testSearchAndFilterByCustomAttribute(): void { $attribute_code = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); @@ -709,7 +842,7 @@ public function testSearchAndFilterByCustomAttribute() * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterByCategoryIdAndCustomAttribute() + public function testFilterByCategoryIdAndCustomAttribute(): void { $category = $this->getCategoryByName->execute('Category 1.2'); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); @@ -857,18 +990,18 @@ public function testFilterByCategoryIdAndCustomAttribute() * @param array $b * @return int */ - private function compareLabels(array $a, array $b) + private function compareLabels(array $a, array $b): int { return strcmp($a['label'], $b['label']); } /** - * Filter by exact match of product url key + * Filter by exact match of product url key * * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterBySingleProductUrlKey() + public function testFilterBySingleProductUrlKey(): void { /** @var Product $product */ $product = $this->productRepository->get('simple-4'); @@ -981,12 +1114,12 @@ public function testFilterBySingleProductUrlKey() } /** - * Filter by multiple product url keys + * Filter by multiple product url keys * * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterByMultipleProductUrlKeys() + public function testFilterByMultipleProductUrlKeys(): void { /** @var Product $product */ $product1 = $this->productRepository->get('simple'); @@ -1064,7 +1197,7 @@ public function testFilterByMultipleProductUrlKeys() * * @return array */ - private function getExpectedFiltersDataSet() + private function getExpectedFiltersDataSet(): array { $attribute = $this->eavConfig->getAttribute('catalog_product', 'test_configurable'); /** @var \Magento\Eav\Api\Data\AttributeOptionInterface[] $options */ @@ -1122,7 +1255,7 @@ private function getExpectedFiltersDataSet() * @param array $expectedFilters * @param string $message */ - private function assertFilters($response, $expectedFilters, $message = '') + private function assertFilters($response, $expectedFilters, $message = ''): void { $this->assertArrayHasKey('filters', $response['products'], 'Product has filters'); $this->assertIsArray(($response['products']['filters']), 'Product filters is not array'); @@ -1149,7 +1282,7 @@ private function assertFilters($response, $expectedFilters, $message = '') * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterWithinSpecificPriceRangeSortedByNameDesc() + public function testFilterWithinSpecificPriceRangeSortedByNameDesc(): void { $query = <<<QUERY @@ -1210,7 +1343,7 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc() * @magentoApiDataFixture Magento/Catalog/_files/category_with_three_products.php * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function testSortByPosition() + public function testSortByPosition(): void { // Get category ID for filtering $category = $this->categoryCollection->addFieldToFilter( @@ -1455,7 +1588,7 @@ protected function getCategoryFilterRelevanceQuery(int $categoryId, string $dire * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testSearchWithFilterWithPageSizeEqualTotalCount() + public function testSearchWithFilterWithPageSizeEqualTotalCount(): void { $query = <<<QUERY @@ -1515,7 +1648,7 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() + public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields(): void { $query = <<<QUERY @@ -1597,7 +1730,7 @@ public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() * * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ - public function testFilterProductsForExactMatchingName() + public function testFilterProductsForExactMatchingName(): void { $query = <<<QUERY @@ -1692,7 +1825,7 @@ public function testFilterProductsForExactMatchingName() /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ - public function testFilteringForProductsFromMultipleCategories() + public function testFilteringForProductsFromMultipleCategories(): void { $categoriesIds = ["4","5","12"]; $query @@ -1744,8 +1877,9 @@ public function testFilteringForProductsFromMultipleCategories() * @return void * @dataProvider filterProductsBySingleCategoryIdDataProvider */ - public function testFilterProductsBySingleCategoryId(string $fieldName, string $queryCategoryId) + public function testFilterProductsBySingleCategoryId(string $fieldName, string $queryCategoryId): void { + CacheCleaner::clean(['config']); if (is_numeric($queryCategoryId)) { $queryCategoryId = (int) $queryCategoryId; } @@ -1841,7 +1975,7 @@ public function testFilterProductsBySingleCategoryId(string $fieldName, string $ * * @throws \Exception */ - public function testSearchAndSortByRelevance() + public function testSearchAndSortByRelevance(): void { $search_term = "blue"; $query @@ -1916,7 +2050,7 @@ public function testSearchAndSortByRelevance() * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterByExactSkuAndSortByPriceDesc() + public function testFilterByExactSkuAndSortByPriceDesc(): void { $query = <<<QUERY @@ -1973,7 +2107,7 @@ public function testFilterByExactSkuAndSortByPriceDesc() * * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ - public function testProductBasicFullTextSearchQuery() + public function testProductBasicFullTextSearchQuery(): void { $textToSearch = 'blue'; $query @@ -2061,7 +2195,7 @@ public function testProductBasicFullTextSearchQuery() * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php */ - public function testProductPartialNameFullTextSearchQuery() + public function testProductPartialNameFullTextSearchQuery(): void { $textToSearch = 'Sim'; $query @@ -2116,11 +2250,10 @@ public function testProductPartialNameFullTextSearchQuery() } QUERY; $prod1 = $this->productRepository->get('simple1'); - $prod2 = $this->productRepository->get('simple2'); $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(1, $response['products']['total_count']); - $filteredProducts = [$prod1, $prod2]; + $filteredProducts = [$prod1]; $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); foreach ($productItemsInResponse as $itemIndex => $itemArray) { $this->assertNotEmpty($itemArray); @@ -2148,7 +2281,7 @@ public function testProductPartialNameFullTextSearchQuery() * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products_with_different_sku_and_name.php */ - public function testProductPartialSkuFullTextSearchQuery() + public function testProductPartialSkuFullTextSearchQuery(): void { $textToSearch = 'prd'; $query @@ -2203,11 +2336,10 @@ public function testProductPartialSkuFullTextSearchQuery() } QUERY; $prod1 = $this->productRepository->get('prd1sku'); - $prod2 = $this->productRepository->get('prd2-sku2'); $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(1, $response['products']['total_count']); - $filteredProducts = [$prod1, $prod2]; + $filteredProducts = [$prod1]; $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); foreach ($productItemsInResponse as $itemIndex => $itemArray) { $this->assertNotEmpty($itemArray); @@ -2230,14 +2362,13 @@ public function testProductPartialSkuFullTextSearchQuery() } /** - * Partial search on hyphenated sku filtered for price and sorted by price and sku + * Partial search on hyphenated sku having visibility as catalog * * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products_with_different_sku_and_name.php */ - public function testProductPartialSkuHyphenatedFullTextSearchQuery() + public function testProductPartialSkuHyphenatedFullTextSearchQuery(): void { - $prod2 = $this->productRepository->get('prd2-sku2'); $textToSearch = 'sku2'; $query = <<<QUERY @@ -2292,28 +2423,7 @@ public function testProductPartialSkuHyphenatedFullTextSearchQuery() QUERY; $response = $this->graphQlQuery($query); - $this->assertEquals(1, $response['products']['total_count']); - - $filteredProducts = [$prod2]; - $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); - foreach ($productItemsInResponse as $itemIndex => $itemArray) { - $this->assertNotEmpty($itemArray); - $this->assertResponseFields( - $productItemsInResponse[$itemIndex][0], - [ - 'sku' => $filteredProducts[$itemIndex]->getSku(), - 'name' => $filteredProducts[$itemIndex]->getName(), - 'price' => [ - 'minimalPrice' => [ - 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), - 'currency' => 'USD' - ] - ] - ] - ] - ); - } + $this->assertEquals(0, $response['products']['total_count']); } /** @@ -2322,7 +2432,7 @@ public function testProductPartialSkuHyphenatedFullTextSearchQuery() * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ - public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() + public function testFilterWithinASpecificPriceRangeSortedByPriceDESC(): void { $prod1 = $this->productRepository->get('simple1'); $prod2 = $this->productRepository->get('simple2'); @@ -2416,7 +2526,7 @@ public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQueryFilterNoMatchingItems() + public function testQueryFilterNoMatchingItems(): void { $query = <<<QUERY @@ -2475,7 +2585,7 @@ public function testQueryFilterNoMatchingItems() * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQueryPageOutOfBoundException() + public function testQueryPageOutOfBoundException(): void { $query = <<<QUERY @@ -2532,7 +2642,7 @@ public function testQueryPageOutOfBoundException() * No filter or search arguments used * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQueryWithNoSearchOrFilterArgumentException() + public function testQueryWithNoSearchOrFilterArgumentException(): void { $query = <<<QUERY @@ -2564,7 +2674,7 @@ public function testQueryWithNoSearchOrFilterArgumentException() * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @magentoApiDataFixture Magento/Catalog/_files/multiple_products_with_few_out_of_stock.php */ - public function testFilterProductsThatAreOutOfStockWithConfigSettings() + public function testFilterProductsThatAreOutOfStockWithConfigSettings(): void { $query = <<<QUERY @@ -2613,7 +2723,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ - public function testInvalidCurrentPage() + public function testInvalidCurrentPage(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('currentPage value must be greater than 0'); @@ -2643,7 +2753,7 @@ public function testInvalidCurrentPage() * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ - public function testInvalidPageSize() + public function testInvalidPageSize(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('pageSize value must be greater than 0'); @@ -2674,7 +2784,7 @@ public function testInvalidPageSize() * @param Product[] $filteredProducts * @param array $actualResponse */ - private function assertProductItems(array $filteredProducts, array $actualResponse) + private function assertProductItems(array $filteredProducts, array $actualResponse): void { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); $count = count($filteredProducts); @@ -2700,7 +2810,13 @@ private function assertProductItems(array $filteredProducts, array $actualRespon } } - private function assertProductItemsWithPriceCheck(array $filteredProducts, array $actualResponse) + /** + * Asserts the different fields of items with price check returned after search query is executed + * + * @param Product[] $filteredProducts + * @param array $actualResponse + */ + private function assertProductItemsWithPriceCheck(array $filteredProducts, array $actualResponse): void { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php index 8e9a76a3a0cfd..091c0e419ee51 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php @@ -19,6 +19,7 @@ class CategoryBlockTest extends GraphQlAbstract { /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 * @magentoApiDataFixture Magento/Catalog/_files/category_tree.php * @magentoApiDataFixture Magento/Cms/_files/block.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php index 071f9a6ea5121..11b584a33b2a5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php @@ -43,9 +43,9 @@ public function testAllGroups() $itemTiers = $response['products']['items'][0]['price_tiers']; $this->assertCount(5, $itemTiers); - $this->assertEquals(8, $this->getValueForQuantity(2, $itemTiers)); - $this->assertEquals(5, $this->getValueForQuantity(3, $itemTiers)); - $this->assertEquals(6, $this->getValueForQuantity(3.2, $itemTiers)); + $this->assertEquals(round(8 * 2, 2), $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(round(5 * 3, 2), $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(round(6 * 3.2, 2), $this->getValueForQuantity(3.2, $itemTiers)); } /** @@ -65,11 +65,11 @@ public function testLoggedInCustomer() $itemTiers = $response['products']['items'][0]['price_tiers']; $this->assertCount(5, $itemTiers); - $this->assertEquals(9.25, $this->getValueForQuantity(2, $itemTiers)); - $this->assertEquals(8.25, $this->getValueForQuantity(3, $itemTiers)); - $this->assertEquals(7.25, $this->getValueForQuantity(5, $itemTiers)); - $this->assertEquals(9.00, $this->getValueForQuantity(7, $itemTiers)); - $this->assertEquals(7.25, $this->getValueForQuantity(8, $itemTiers)); + $this->assertEquals(round(9.25 * 2, 2), $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(round(8.25 * 3, 2), $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(round(7.25 * 5, 2), $this->getValueForQuantity(5, $itemTiers)); + $this->assertEquals(round(9.00 * 7, 2), $this->getValueForQuantity(7, $itemTiers)); + $this->assertEquals(round(7.25 * 8, 2), $this->getValueForQuantity(8, $itemTiers)); } /** @@ -98,9 +98,9 @@ public function testSecondStoreViewWithCurrencyRate() $itemTiers = $response['products']['items'][0]['price_tiers']; $this->assertCount(5, $itemTiers); - $this->assertEquals(round(9.25 * $rate, 2), $this->getValueForQuantity(2, $itemTiers)); - $this->assertEquals(round(8.25 * $rate, 2), $this->getValueForQuantity(3, $itemTiers)); - $this->assertEquals(round(7.25 * $rate, 2), $this->getValueForQuantity(5, $itemTiers)); + $this->assertEquals(round((9.25 * 2) * $rate, 2), $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(round((8.25 * 3) * $rate, 2), $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(round((7.25 * 5) * $rate, 2), $this->getValueForQuantity(5, $itemTiers)); } /** @@ -113,8 +113,8 @@ public function testGetLowestPriceForGuest() $response = $this->graphQlQuery($query); $itemTiers = $response['products']['items'][0]['price_tiers']; $this->assertCount(2, $itemTiers); - $this->assertEquals(round(8.25, 2), $this->getValueForQuantity(7, $itemTiers)); - $this->assertEquals(round(7.25, 2), $this->getValueForQuantity(8, $itemTiers)); + $this->assertEquals(round((8.25 * 7), 2), $this->getValueForQuantity(7, $itemTiers)); + $this->assertEquals(round((7.25 * 8), 2), $this->getValueForQuantity(8, $itemTiers)); } /** @@ -147,7 +147,9 @@ public function testProductTierPricesAreCorrectlyReturned() if (in_array($item['sku'], $productsWithTierPrices)) { $this->assertCount(1, $response['products']['items'][$key]['price_tiers']); } else { - $this->assertCount(0, $response['products']['items'][$key]['price_tiers']); + if (empty($response['products']['items'][$key]['price_tiers'])) { + $this->assertCount(0, $response['products']['items'][$key]['price_tiers']); + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php new file mode 100644 index 0000000000000..a0fa6d4e083fa --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/AttributesMetadataTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogGraphQl; + +use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Test\Fixture\Attribute; +use Magento\Catalog\Test\Fixture\CategoryAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +#[ + DataFixture( + CategoryAttribute::class, + [ + 'frontend_input' => 'multiselect', + 'is_filterable_in_search' => true, + 'position' => 4, + 'apply_to' => 'category' + ], + 'category_attribute' + ), + DataFixture( + Attribute::class, + [ + 'frontend_input' => 'multiselect', + 'is_filterable_in_search' => true, + 'position' => 5, + ], + 'product_attribute' + ), +] +class AttributesMetadataTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ...on CatalogAttributeMetadata { + is_filterable_in_search + is_searchable + is_filterable + is_comparable + is_html_allowed_on_front + is_used_for_price_rules + is_wysiwyg_enabled + is_used_for_promo_rules + used_in_product_listing + apply_to + } + } + errors { + type + message + } + } +} +QRY; + + /** + * @return void + * @throws \Exception + */ + public function testMetadataProduct(): void + { + /** @var ProductAttributeInterface $productAttribute */ + $productAttribute = DataFixtureStorageManager::getStorage()->get('product_attribute'); + + $result = $this->graphQlQuery( + sprintf( + self::QUERY, + $productAttribute->getAttributeCode(), + ProductAttributeInterface::ENTITY_TYPE_CODE + ) + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $productAttribute->getAttributeCode(), + 'label' => $productAttribute->getDefaultFrontendLabel(), + 'entity_type' => strtoupper(ProductAttributeInterface::ENTITY_TYPE_CODE), + 'frontend_input' => 'MULTISELECT', + 'is_required' => false, + 'default_value' => $productAttribute->getDefaultValue(), + 'is_unique' => false, + 'is_filterable_in_search' => true, + 'is_searchable' => false, + 'is_filterable' => false, + 'is_comparable' => false, + 'is_html_allowed_on_front' => true, + 'is_used_for_price_rules' => false, + 'is_wysiwyg_enabled' => false, + 'is_used_for_promo_rules' => false, + 'used_in_product_listing' => false, + 'apply_to' => null, + ] + ], + 'errors' => [] + ] + ], + $result + ); + } + + /** + * @return void + * @throws \Exception + */ + public function testMetadataCategory(): void + { + /** @var CategoryAttributeInterface $categoryAttribute */ + $categoryAttribute = DataFixtureStorageManager::getStorage()->get('category_attribute'); + + $result = $this->graphQlQuery( + sprintf( + self::QUERY, + $categoryAttribute->getAttributeCode(), + CategoryAttributeInterface::ENTITY_TYPE_CODE + ) + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $categoryAttribute->getAttributeCode(), + 'label' => $categoryAttribute->getDefaultFrontendLabel(), + 'entity_type' => strtoupper(CategoryAttributeInterface::ENTITY_TYPE_CODE), + 'frontend_input' => 'MULTISELECT', + 'is_required' => false, + 'default_value' => $categoryAttribute->getDefaultValue(), + 'is_unique' => false, + 'is_filterable_in_search' => true, + 'is_searchable' => false, + 'is_filterable' => false, + 'is_comparable' => false, + 'is_html_allowed_on_front' => true, + 'is_used_for_price_rules' => false, + 'is_wysiwyg_enabled' => false, + 'is_used_for_promo_rules' => false, + 'used_in_product_listing' => false, + 'apply_to' => ['CATEGORY'], + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/PriceAttributeOptionsLabelTranslateTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/PriceAttributeOptionsLabelTranslateTest.php index 936d8be75de59..643d2131bb995 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/PriceAttributeOptionsLabelTranslateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/PriceAttributeOptionsLabelTranslateTest.php @@ -9,14 +9,17 @@ use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Directory\Helper\Data as LocaleConfig; use Magento\Eav\Model\Entity\Attribute\FrontendLabel; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\TestFramework\Fixture\Config; use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Translation\Test\Fixture\Translation as TranslationFixture; /** * Test class to verify the translated price attribute option label based on the store view. @@ -98,8 +101,7 @@ public function testValidatePriceAttributeOptionsLabelTranslationForSecondStoreV $priceAttributeOptionLabel = $attribute['label']; } } - - $this->assertEquals($priceAttributeOptionLabel, 'Price View2'); + $this->assertEquals('Price View2', $priceAttributeOptionLabel); } /** @@ -133,4 +135,72 @@ private function getProductsQueryWithAggregations() : string } QUERY; } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + #[ + DataFixture( + TranslationFixture::class, + [ + 'string' => 'Price View2', + 'translate' => 'Preisansicht', + 'locale' => 'de_DE', + ] + ), + DataFixture( + StoreFixture::class, + [ + 'code' => 'view2', + 'name' => 'view2' + ], + as: 'view2' + ), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple' + ], + as: 'product' + ), + Config(LocaleConfig::XML_PATH_DEFAULT_LOCALE, 'de_DE', 'store', 'view2'), + ] + + public function testValidateAggregateAttributeOptionsInLabelLocaleTranslationForSecondStoreView(): void + { + $attributeCode = 'price'; + $secondStoreViewFixtureName = 'view2'; + $attributeStoreFrontLabelForSecondStoreView = 'Price View2'; + + //Updating price attribute storefront option label for the second store view. + $attributeRepository = $this->objectManager->create(ProductAttributeRepositoryInterface::class); + + $priceAttribute = $attributeRepository->get($attributeCode); + + $frontendLabelAttribute = $this->objectManager->get(FrontendLabel::class); + $frontendLabelAttribute->setStoreId( + $this->fixture->get($secondStoreViewFixtureName)->getId() + ); + $frontendLabelAttribute->setLabel($attributeStoreFrontLabelForSecondStoreView); + + $frontendLabels = $priceAttribute->getFrontendLabels(); + $frontendLabels[] = $frontendLabelAttribute; + + $priceAttribute->setFrontendLabels($frontendLabels); + $attributeRepository->save($priceAttribute); + $query = $this->getProductsQueryWithAggregations(); + $headers = ['Store' => $secondStoreViewFixtureName]; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $aggregationAttributes = $response['products']['aggregations']; + $priceAttributeOptionLabel = ''; + + foreach ($aggregationAttributes as $attribute) { + if ($attribute['attribute_code'] === $attributeCode) { + $priceAttributeOptionLabel = $attribute['label']; + } + } + + $this->assertEquals('Preisansicht', $priceAttributeOptionLabel); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php index e8d63d8cf64c7..e853a8d62594a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php @@ -7,6 +7,9 @@ namespace Magento\GraphQl\CatalogGraphQl; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Indexer\Model\IndexerFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\TestFramework\Fixture\DataFixtureStorageManager; @@ -20,17 +23,125 @@ */ class ProductSearchTest extends GraphQlAbstract { + #[ + DataFixture(CategoryFixture::class, as: 'cat1'), + DataFixture( + ProductFixture::class, + [ + 'category_ids' => ['$cat1.id$'], + ], + 'product' + ) + ] + public function testSearchProductsWithCategoriesAliasPresentInQuery(): void + { + $this->reindexCatalogCategory(); + /** @var \Magento\Catalog\Model\Product $product */ + $product = DataFixtureStorageManager::getStorage()->get('product'); + /** @var \Magento\Catalog\Model\Category $category */ + $category = DataFixtureStorageManager::getStorage()->get('cat1'); + $response = $this->graphQlQuery($this->getProductSearchQueryWithCategoriesAlias($product->getSku())); + + $this->assertNotEmpty($response['products']); + $this->assertNotEmpty($response['products']['items']); + $this->assertEquals( + $category->getUrlKey(), + $response['products']['items'][0]['custom_categories'][0]['url_key'] + ); + } + /** - * @var ObjectManager|null + * Make catalog_category reindex. + * + * @return void + * @throws \Throwable */ - private $objectManager; + private function reindexCatalogCategory(): void + { + $indexerFactory = Bootstrap::getObjectManager()->create(IndexerFactory::class); + $indexer = $indexerFactory->create(); + $indexer->load('catalog_category_product')->reindexAll(); + } + + #[ + DataFixture(Product::class, as: 'product') + ] + public function testSearchProductsWithSkuEqFilterQuery(): void + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = DataFixtureStorageManager::getStorage()->get('product'); + $response = $this->graphQlQuery($this->getProductSearchQuery($product->getName(), $product->getSku())); + + $this->assertNotEmpty($response['products']); + $this->assertEquals(1, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['items']); + $this->assertEquals($product->getName(), $response['products']['items'][0]['name']); + $this->assertEquals($product->getSku(), $response['products']['items'][0]['sku']); + } + + #[ + DataFixture(Product::class, as: 'product1'), + DataFixture(Product::class, as: 'product2'), + DataFixture(Product::class, as: 'product3') + ] + public function testSearchProductsWithMultipleSkuInFilterQuery(): void + { + /** @var \Magento\Catalog\Model\Product $product */ + $response = $this->graphQlQuery( + $this->getProductSearchQueryWithMultipleSkusFilter([ + DataFixtureStorageManager::getStorage()->get('product1'), + DataFixtureStorageManager::getStorage()->get('product2'), + DataFixtureStorageManager::getStorage()->get('product3') + ], "simple") + ); + + $this->assertNotEmpty($response['products']); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['items']); + } + + #[ + DataFixture(Product::class, as: 'product1'), + DataFixture(Product::class, as: 'product2'), + DataFixture(Indexer::class, as: 'indexer') + ] + public function testSearchSuggestions(): void + { + $response = $this->graphQlQuery($this->getSearchQueryWithSuggestions()); + $this->assertNotEmpty($response['products']); + $this->assertEmpty($response['products']['items']); + $this->assertNotEmpty($response['products']['suggestions']); + } /** - * Test setup + * Get a query which contains alias for product categories data. + * + * @param string $productSku + * @return string */ - protected function setUp(): void + private function getProductSearchQueryWithCategoriesAlias(string $productSku): string { - $this->objectManager = Bootstrap::getObjectManager(); + return <<<QUERY + { + products(filter: { + sku: { + eq: "{$productSku}" + }}) + { + items { + name + sku + categories { + uid + name + } + custom_categories: categories { + url_key + } + } + } + } + QUERY; } /** @@ -47,11 +158,11 @@ private function getProductSearchQuery(string $productName, string $productSku): products(filter: { sku: { eq: "{$productSku}" - }}, - search: "$productName", - sort: {}, - pageSize: 200, - currentPage: 1) + }}, + search: "$productName", + sort: {}, + pageSize: 200, + currentPage: 1) { total_count page_info { @@ -86,11 +197,11 @@ private function getProductSearchQueryWithMultipleSkusFilter(array $products, st "{$products[1]->getSku()}", "{$products[2]->getSku()}" ] - }}, - search: "$product", - sort: {}, - pageSize: 200, - currentPage: 1) + }}, + search: "$product", + sort: {}, + pageSize: 200, + currentPage: 1) { total_count page_info { @@ -130,54 +241,4 @@ private function getSearchQueryWithSuggestions(): string } QUERY; } - - #[ - DataFixture(Product::class, as: 'product') - ] - public function testSearchProductsWithSkuEqFilterQuery(): void - { - /** @var \Magento\Catalog\Model\Product $product */ - $product = DataFixtureStorageManager::getStorage()->get('product'); - $response = $this->graphQlQuery($this->getProductSearchQuery($product->getName(), $product->getSku())); - - $this->assertNotEmpty($response['products']); - $this->assertEquals(1, $response['products']['total_count']); - $this->assertNotEmpty($response['products']['items']); - $this->assertEquals($product->getName(), $response['products']['items'][0]['name']); - $this->assertEquals($product->getSku(), $response['products']['items'][0]['sku']); - } - - #[ - DataFixture(Product::class, as: 'product1'), - DataFixture(Product::class, as: 'product2'), - DataFixture(Product::class, as: 'product3') - ] - public function testSearchProductsWithMultipleSkuInFilterQuery(): void - { - /** @var \Magento\Catalog\Model\Product $product */ - $response = $this->graphQlQuery( - $this->getProductSearchQueryWithMultipleSkusFilter([ - DataFixtureStorageManager::getStorage()->get('product1'), - DataFixtureStorageManager::getStorage()->get('product2'), - DataFixtureStorageManager::getStorage()->get('product3') - ], "simple") - ); - - $this->assertNotEmpty($response['products']); - $this->assertEquals(3, $response['products']['total_count']); - $this->assertNotEmpty($response['products']['items']); - } - - #[ - DataFixture(Product::class, as: 'product1'), - DataFixture(Product::class, as: 'product2'), - DataFixture(Indexer::class, as: 'indexer') - ] - public function testSearchSuggestions(): void - { - $response = $this->graphQlQuery($this->getSearchQueryWithSuggestions()); - $this->assertNotEmpty($response['products']); - $this->assertEmpty($response['products']['items']); - $this->assertNotEmpty($response['products']['suggestions']); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php index 07de3e1641b60..fa9a2f21c8530 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php @@ -38,6 +38,7 @@ protected function setUp(): void /** * Verify the fields of CMS Block selected by identifiers * + * @magentoConfigFixture default_store web/seo/use_rewrites 1 * @magentoApiDataFixture Magento/Cms/_files/blocks.php */ public function testGetCmsBlock() @@ -71,6 +72,7 @@ public function testGetCmsBlock() /** * Verify the fields of CMS Block selected by block_id * + * @magentoConfigFixture default_store web/seo/use_rewrites 1 * @magentoApiDataFixture Magento/Cms/_files/blocks.php */ public function testGetCmsBlockByBlockId() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/BlockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/BlockTest.php new file mode 100644 index 0000000000000..f67089a4a1c1c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/BlockTest.php @@ -0,0 +1,455 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CmsGraphQl\Model\Resolver; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Model\Block; +use Magento\CmsGraphQl\Model\Resolver\Blocks; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQl\ResolverCacheAbstract; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; +use Magento\Widget\Model\Template\FilterEmulate; + +/** + * Test for cms block resolver cache + */ +class BlockTest extends ResolverCacheAbstract +{ + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var FilterEmulate + */ + private $widgetFilter; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + protected function setUp(): void + { + $objectManager = ObjectManager::getInstance(); + $this->blockRepository = $objectManager->get(BlockRepositoryInterface::class); + $this->graphQlResolverCache = $objectManager->get(GraphQlResolverCache::class); + $this->widgetFilter = $objectManager->get(FilterEmulate::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + + parent::setUp(); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoDataFixture Magento/Cms/_files/blocks.php + */ + public function testCmsSingleBlockResolverCacheAndInvalidationAsGuest() + { + $block = $this->blockRepository->getById('enabled_block'); + + $query = $this->getQuery([ + $block->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($query); + + $cacheIdentityString = $this->getResolverCacheKeyFromBlocks([$block]); + + $cacheEntry = $this->graphQlResolverCache->load($cacheIdentityString); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromBlocks([$block]), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheIdentityAndBlocks( + $cacheIdentityString, + [$block] + ); + + // assert that cache is invalidated after block content change + $block->setContent('New Content'); + $this->blockRepository->save($block); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheIdentityString), + 'Cache entry should be invalidated after block content change' + ); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoDataFixture Magento/Cms/_files/block.php + * @magentoDataFixture Magento/Cms/_files/blocks.php + */ + public function testCmsMultipleBlockResolverCacheAndInvalidationAsGuest() + { + $block1 = $this->blockRepository->getById('enabled_block'); + $block2 = $this->blockRepository->getById('fixture_block'); + + $query = $this->getQuery([ + $block1->getIdentifier(), + $block2->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyFromBlocks([ + $block1, + $block2, + ]); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromBlocks([$block1, $block2]), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheIdentityAndBlocks( + $cacheKey, + [$block1, $block2] + ); + + // assert that cache is invalidated after a single block content change + $block2->setContent('New Content'); + $this->blockRepository->save($block2); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry should be invalidated after block content change' + ); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoDataFixture Magento/Cms/_files/block.php + */ + public function testCmsBlockResolverCacheInvalidatesWhenBlockGetsDeleted() + { + $block = $this->blockRepository->getById('fixture_block'); + + $query = $this->getQuery([ + $block->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyFromBlocks([$block]); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromBlocks([$block]), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheIdentityAndBlocks( + $cacheKey, + [$block] + ); + + // assert that cache is invalidated after block deletion + $this->blockRepository->delete($block); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry should be invalidated after block deletion' + ); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoDataFixture Magento/Cms/_files/blocks.php + */ + public function testCmsBlockResolverCacheInvalidatesWhenBlockGetsDisabled() + { + $block = $this->blockRepository->getById('enabled_block'); + + $query = $this->getQuery([ + $block->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyFromBlocks([$block]); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromBlocks([$block]), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheIdentityAndBlocks( + $cacheKey, + [$block] + ); + + // assert that cache is invalidated after block disablement + $block->setIsActive(false); + $this->blockRepository->save($block); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry should be invalidated after block disablement' + ); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoDataFixture Magento/Cms/_files/block.php + * @magentoDataFixture Magento/Store/_files/second_store.php + */ + public function testCmsBlockResolverCacheIsInvalidatedAfterChangingItsStoreView() + { + /** @var Block $block */ + $block = $this->blockRepository->getById('fixture_block'); + + $query = $this->getQuery([ + $block->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyFromBlocks([$block]); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromBlocks([$block]), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheIdentityAndBlocks( + $cacheKey, + [$block] + ); + + // assert that cache is invalidated after changing block's store view + $secondStoreViewId = $this->storeManager->getStore('fixture_second_store')->getId(); + $block->setStoreId($secondStoreViewId); + $this->blockRepository->save($block); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry should be invalidated after changing block\'s store view' + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @return void + */ + public function testCmsBlockResolverCacheDoesNotSaveNonExistentCmsBlock() + { + $nonExistentBlock = ObjectManager::getInstance()->create(BlockInterface::class); + $nonExistentBlock->setIdentifier('non-existent-block'); + + $query = $this->getQuery([$nonExistentBlock->getIdentifier()]); + + try { + $this->graphQlQueryWithResponseHeaders($query); + $this->fail('Expected exception was not thrown'); + } catch (ResponseContainsErrorsException $e) { + // expected exception + } + + $cacheIdentityString = $this->getResolverCacheKeyFromBlocks([$nonExistentBlock]); + + $this->assertFalse( + $this->graphQlResolverCache->load($cacheIdentityString) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/_files/block.php + * @magentoDataFixture Magento/Cms/_files/blocks.php + */ + public function testCmsBlockResolverCacheRetainsEntriesThatHaveNotBeenUpdated() + { + // query block1 + $block1 = $this->blockRepository->getById('fixture_block'); + + $queryBlock1 = $this->getQuery([ + $block1->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($queryBlock1); + + $cacheKeyBlock1 = $this->getResolverCacheKeyFromBlocks([$block1]); + + // query block2 + $block2 = $this->blockRepository->getById('enabled_block'); + + $queryBlock2 = $this->getQuery([ + $block2->getIdentifier(), + ]); + + $this->graphQlQueryWithResponseHeaders($queryBlock2); + + $cacheKeyBlock2 = $this->getResolverCacheKeyFromBlocks([$block2]); + + // assert both cache entries are present + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyBlock1), + 'Cache entry for block1 should be present' + ); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyBlock2), + 'Cache entry for block2 should be present' + ); + + // assert that cache is invalidated after block1 update + $block1->setContent('Updated content'); + $this->blockRepository->save($block1); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKeyBlock1), + 'Cache entry for block1 should be invalidated after block1 update' + ); + + // assert that cache is not invalidated after block1 update + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyBlock2), + 'Cache entry for block2 should be present after block1 update' + ); + } + + private function getQuery(array $identifiers): string + { + $identifiersStr = $this->getQuotedBlockIdentifiersListAsString($identifiers); + + return <<<QUERY +{ + cmsBlocks(identifiers: [$identifiersStr]) { + items { + title + content + identifier + } + } +} +QUERY; + } + + /** + * @param string[] $identifiers + * @return string + */ + private function getQuotedBlockIdentifiersListAsString(array $identifiers): string + { + return implode(',', array_map(function (string $identifier) { + return "\"$identifier\""; + }, $identifiers)); + } + + /** + * @param BlockInterface[] $blocks + * @return array + */ + private function generateExpectedDataFromBlocks(array $blocks): array + { + $expectedBlockData = []; + + foreach ($blocks as $block) { + $expectedBlockData[$block->getIdentifier()] = [ + 'block_id' => $block->getId(), + 'identifier' => $block->getIdentifier(), + 'title' => $block->getTitle(), + 'content' => $this->widgetFilter->filterDirective($block->getContent()), + ]; + } + + return [ + 'items' => $expectedBlockData, + ]; + } + + /** + * @param string $cacheIdentityString + * @param BlockInterface[] $blocks + * @return void + * @throws \Zend_Cache_Exception + */ + private function assertTagsByCacheIdentityAndBlocks(string $cacheIdentityString, array $blocks): void + { + $lowLevelFrontendCache = $this->graphQlResolverCache->getLowLevelFrontend(); + $cacheIdPrefix = $lowLevelFrontendCache->getOption('cache_id_prefix'); + $metadatas = $lowLevelFrontendCache->getMetadatas($cacheIdentityString); + $tags = $metadatas['tags']; + + $expectedTags = [ + $cacheIdPrefix . strtoupper(GraphQlResolverCache::CACHE_TAG), + $cacheIdPrefix . 'MAGE', + ]; + + foreach ($blocks as $block) { + $expectedTags[] = $cacheIdPrefix . strtoupper(Block::CACHE_TAG) . '_' . $block->getId(); + $expectedTags[] = $cacheIdPrefix . strtoupper(Block::CACHE_TAG . '_' . $block->getIdentifier()); + } + + $this->assertEqualsCanonicalizing( + $expectedTags, + $tags + ); + } + + /** + * @param array $response + * @param BlockInterface[] $blocks + * @return string + */ + private function getResolverCacheKeyFromBlocks(array $blocks): string + { + $resolverMock = $this->getMockBuilder(Blocks::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var ProviderInterface $cacheKeyCalculatorProvider */ + $cacheKeyCalculatorProvider = ObjectManager::getInstance()->get(ProviderInterface::class); + $cacheKeyFactor = $cacheKeyCalculatorProvider + ->getKeyCalculatorForResolver($resolverMock) + ->calculateCacheKey(); + + $blockIdentifiers = array_map(function (BlockInterface $block) { + return $block->getIdentifier(); + }, $blocks); + + $cacheKeyQueryPayloadMetadata = sprintf(Blocks::class . '\Interceptor%s', json_encode([ + 'identifiers' => $blockIdentifiers, + ])); + + $cacheKeyParts = [ + GraphQlResolverCache::CACHE_TAG, + $cacheKeyFactor, + sha1($cacheKeyQueryPayloadMetadata) + ]; + + // strtoupper is called in \Magento\Framework\Cache\Frontend\Adapter\Zend::_unifyId + return strtoupper(implode('_', $cacheKeyParts)); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/PageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/PageTest.php new file mode 100644 index 0000000000000..f5afd6b54b41c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsGraphQl/Model/Resolver/PageTest.php @@ -0,0 +1,536 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CmsGraphQl\Model\Resolver; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page as CmsPage; +use Magento\Cms\Model\PageRepository; +use Magento\CmsGraphQl\Model\Resolver\Page; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Cache\Frontend\Factory as CacheFrontendFactory; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQl\ResolverCacheAbstract; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PageTest extends ResolverCacheAbstract +{ + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var PageRepository + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp(): void + { + $objectManager = ObjectManager::getInstance(); + + $this->graphQlResolverCache = $objectManager->get(GraphQlResolverCache::class); + $this->pageRepository = $objectManager->get(PageRepository::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + + parent::setUp(); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsPageResolverCacheAndInvalidationAsGuest() + { + $page = $this->getPageByTitle('Page with 1column layout'); + + $query = $this->getQuery($page->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyForPage($page); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromPage($page), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheKeyAndPage($cacheKey, $page); + + // update CMS page and assert cache is invalidated + $page->setContent('something different'); + $this->pageRepository->save($page); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry still exists for CMS page' + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsPageResolverCacheAndInvalidationAsCustomer() + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->mockCustomerUserInfoContext($customer); + + $authHeader = [ + 'Authorization' => 'Bearer ' . $this->customerTokenService->createCustomerAccessToken( + 'customer@example.com', + 'password' + ) + ]; + + $page = $this->getPageByTitle('Page with 1column layout'); + $query = $this->getQuery($page->getIdentifier()); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + $authHeader + ); + + $cacheKey = $this->getResolverCacheKeyForPage($page); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromPage($page), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheKeyAndPage($cacheKey, $page); + + // update CMS page and assert cache is invalidated + $page->setIdentifier('1-column-page-different-identifier'); + $this->pageRepository->save($page); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey), + 'Cache entry still exists for CMS page' + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + * @throws \ReflectionException + */ + public function testCmsPageResolverCacheWithPostRequest() + { + $page = $this->getPageByTitle('Page with 1column layout'); + + $getGraphQlClient = new \ReflectionMethod($this, 'getGraphQlClient'); + $getGraphQlClient->setAccessible(true); + + $query = $this->getQuery($page->getIdentifier()); + $getGraphQlClient->invoke($this)->postWithResponseHeaders($query); + + $cacheKey = $this->getResolverCacheKeyForPage($page); + + $cacheEntry = $this->graphQlResolverCache->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromPage($page), + $cacheEntryDecoded + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsPageResolverCacheGeneratesSeparateEntriesBasedOnArgumentsAndContext() + { + $titles = ['Page with 1column layout', 'Page with unavailable layout']; + + $authHeader = [ + 'Authorization' => 'Bearer ' . $this->customerTokenService->createCustomerAccessToken( + 'customer@example.com', + 'password' + ) + ]; + + $customer = $this->customerRepository->get('customer@example.com'); + + foreach ($titles as $title) { + $page = $this->getPageByTitle($title); + + // query $page as guest + $query = $this->getQuery($page->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $this->mockGuestUserInfoContext(); + $resolverCacheKeyForGuestQuery = $this->getResolverCacheKeyForPage($page); + + $cacheEntry = $this->graphQlResolverCache->load($resolverCacheKeyForGuestQuery); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromPage($page), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheKeyAndPage($resolverCacheKeyForGuestQuery, $page); + + $resolverCacheKeys[] = $resolverCacheKeyForGuestQuery; + + // query $page as customer + $query = $this->getQuery($page->getIdentifier()); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + $authHeader + ); + + $this->mockCustomerUserInfoContext($customer); + $resolverCacheKeyForUserQuery = $this->getResolverCacheKeyForPage($page); + + $cacheEntry = $this->graphQlResolverCache->load($resolverCacheKeyForUserQuery); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEqualsCanonicalizing( + $this->generateExpectedDataFromPage($page), + $cacheEntryDecoded + ); + + $this->assertTagsByCacheKeyAndPage($resolverCacheKeyForUserQuery, $page); + + $resolverCacheKeys[] = $resolverCacheKeyForUserQuery; + } + + // assert that every cache key is unique + $this->assertCount(count($resolverCacheKeys), array_unique($resolverCacheKeys)); + + foreach ($resolverCacheKeys as $cacheKey) { + $this->assertNotFalse($this->graphQlResolverCache->load($cacheKey)); + } + + // invalidate first page and assert first two cache keys (guest and user) are invalidated, + // while the rest are not + $page = $this->getPageByTitle($titles[0]); + $page->setMetaDescription('whatever'); + $this->pageRepository->save($page); + + list($page1GuestKey, $page1UserKey, $page2GuestKey, $page2UserKey) = $resolverCacheKeys; + + $this->assertFalse($this->graphQlResolverCache->load($page1GuestKey)); + $this->assertFalse($this->graphQlResolverCache->load($page1UserKey)); + + $this->assertNotFalse($this->graphQlResolverCache->load($page2GuestKey)); + $this->assertNotFalse($this->graphQlResolverCache->load($page2UserKey)); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + * @throws \Magento\Framework\Exception\CouldNotDeleteException + */ + public function testCmsPageResolverCacheInvalidatesWhenPageGetsDeleted() + { + // cache page1 + $page1 = $this->getPageByTitle('Page with 1column layout'); + + $query = $this->getQuery($page1->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKeyPage1 = $this->getResolverCacheKeyForPage($page1); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage1) + ); + + // cache page2 + $page2 = $this->getPageByTitle('Page with unavailable layout'); + + $query = $this->getQuery($page2->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKeyPage2 = $this->getResolverCacheKeyForPage($page2); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage2) + ); + + // delete page1 and assert cache is invalidated + $this->pageRepository->delete($page1); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKeyPage1), + 'Cache entry still exists for deleted CMS page' + ); + + // assert page2 cache entry still exists + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage2) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsPageResolverCacheInvalidatesWhenPageGetsDisabled() + { + // cache page1 + $page1 = $this->getPageByTitle('Page with 1column layout'); + + $query = $this->getQuery($page1->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKeyPage1 = $this->getResolverCacheKeyForPage($page1); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage1) + ); + + // cache page2 + $page2 = $this->getPageByTitle('Page with unavailable layout'); + + $query = $this->getQuery($page2->getIdentifier()); + $this->graphQlQueryWithResponseHeaders($query); + + $cacheKeyPage2 = $this->getResolverCacheKeyForPage($page2); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage2) + ); + + // disable page 1 + $page1->setIsActive(false); + $this->pageRepository->save($page1); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKeyPage1), + 'Cache entry still exists for disabled CMS page' + ); + + // assert page2 cache entry still exists + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKeyPage2) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsPageResolverCacheDoesNotSaveNonExistentCmsPage() + { + $nonExistentPage = ObjectManager::getInstance()->create(PageInterface::class); + $nonExistentPage->setIdentifier('non-existent-page'); + + $query = $this->getQuery($nonExistentPage->getIdentifier()); + + try { + $this->graphQlQueryWithResponseHeaders($query); + $this->fail('Expected exception was not thrown'); + } catch (ResponseContainsErrorsException $e) { + // expected exception + } + + $cacheKey = $this->getResolverCacheKeyForPage($nonExistentPage); + + $this->assertFalse( + $this->graphQlResolverCache->load($cacheKey) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCmsResolverCacheIsInvalidatedAfterChangingItsStoreView() + { + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->getPageByTitle('Page with 1column layout'); + + // query first page in default store and assert cache entry is created; use default store header + $query = $this->getQuery($page->getIdentifier()); + + $this->graphQlQueryWithResponseHeaders( + $query + ); + + $cacheKey = $this->getResolverCacheKeyForPage($page); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKey) + ); + + // change store id of page + $secondStoreViewId = $this->storeManager->getStore('fixture_second_store')->getId(); + $page->setStoreId($secondStoreViewId); + $this->pageRepository->save($page); + + // assert cache entry is invalidated + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey) + ); + } + + /** + * Test that resolver cache is saved with default TTL + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testCacheExpirationTimeUsesDefaultDirective() + { + $page = $this->getPageByTitle('Page with 1column layout'); + $query = $this->getQuery($page->getIdentifier()); + $this->graphQlQueryWithResponseHeaders( + $query + ); + + $cacheKey = $this->getResolverCacheKeyForPage($page); + + $lowLevelFrontendCache = $this->graphQlResolverCache->getLowLevelFrontend(); + $metadatas = $lowLevelFrontendCache->getMetadatas($cacheKey); + + $this->assertEquals( + $metadatas['mtime'] + CacheFrontendFactory::DEFAULT_LIFETIME, + $metadatas['expire'] + ); + } + + private function generateExpectedDataFromPage(PageInterface $page): array + { + return [ + 'page_id' => $page->getId(), + 'identifier' => $page->getIdentifier(), + 'url_key' => $page->getIdentifier(), + 'title' => $page->getTitle(), + 'content' => $page->getContent(), + 'content_heading' => $page->getContentHeading(), + 'page_layout' => $page->getPageLayout(), + 'meta_keywords' => $page->getMetaKeywords(), + 'meta_title' => $page->getMetaTitle(), + 'meta_description' => $page->getMetaDescription(), + ]; + } + + private function assertTagsByCacheKeyAndPage(string $cacheKey, PageInterface $page): void + { + $lowLevelFrontendCache = $this->graphQlResolverCache->getLowLevelFrontend(); + $cacheIdPrefix = $lowLevelFrontendCache->getOption('cache_id_prefix'); + $metadatas = $lowLevelFrontendCache->getMetadatas($cacheKey); + $tags = $metadatas['tags']; + + $this->assertEqualsCanonicalizing( + [ + $cacheIdPrefix . strtoupper(CmsPage::CACHE_TAG) . '_' . $page->getId(), + $cacheIdPrefix . strtoupper(GraphQlResolverCache::CACHE_TAG), + $cacheIdPrefix . 'MAGE', + ], + $tags + ); + } + + private function getPageByTitle(string $title): PageInterface + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('title', $title) + ->create(); + + $pages = $this->pageRepository->getList($searchCriteria)->getItems(); + + /** @var PageInterface $page */ + $page = reset($pages); + + return $page; + } + + private function getQuery(string $identifier): string + { + return <<<QUERY +{ + cmsPage(identifier: "$identifier") { + title + } +} +QUERY; + } + + /** + * Create resolver key with key calculator retriever vis the actual key provider. + * + * @param PageInterface $page + * @return string + */ + private function getResolverCacheKeyForPage(PageInterface $page): string + { + $resolverMock = $this->getMockBuilder(Page::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProviderInterface $cacheKeyCalculatorProvider */ + $cacheKeyCalculatorProvider = ObjectManager::getInstance()->get(ProviderInterface::class); + $cacheKeyFactor = $cacheKeyCalculatorProvider->getKeyCalculatorForResolver($resolverMock)->calculateCacheKey(); + + $cacheKeyQueryPayloadMetadata = sprintf(Page::class . '\Interceptor%s', json_encode([ + 'identifier' => $page->getIdentifier(), + ])); + + $cacheKeyParts = [ + GraphQlResolverCache::CACHE_TAG, + $cacheKeyFactor, + sha1($cacheKeyQueryPayloadMetadata) + ]; + + // strtoupper is called in \Magento\Framework\Cache\Frontend\Adapter\Zend::_unifyId + return strtoupper(implode('_', $cacheKeyParts)); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ContactUs/ContactUsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ContactUs/ContactUsTest.php new file mode 100644 index 0000000000000..a49d33e7468d2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ContactUs/ContactUsTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ContactUs; + +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +#[ + Config("contact/contact/enabled", "1") +] +class ContactUsTest extends GraphQlAbstract +{ + /** + * Successfuly send contact us form + */ + public function testContactUsSuccess() + { + $query = <<<MUTATION +mutation { + contactUs(input: { + comment:"Test Contact Us", + email:"test@adobe.com", + name:"John Doe", + telephone:"1111111111" + }) + { + status + } +} +MUTATION; + + $expected = [ + "contactUs" => [ + "status" => true + ] + ]; + $response = $this->graphQlMutation($query, [], '', []); + $this->assertEquals($expected, $response, "Contact Us form can not be send"); + } + + /** + * Failed send contact us form - missing email + */ + public function testContactUsBadEmail() + { + $query = <<<MUTATION +mutation { + contactUs(input: { + comment:"Test Contact Us", + name:"John Doe", + email:"adobe.com", + telephone:"1111111111" + }) + { + status + } +} +MUTATION; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage( + 'GraphQL response contains errors: The email address is invalid. Verify the email address and try again.' + ); + $this->graphQlMutation($query, [], '', []); + } + + /** + * Failed send contact us form - missing name + */ + public function testContactUsMissingName() + { + $query = <<<MUTATION +mutation { + contactUs(input: { + comment:"Test Contact Us", + email:"test@adobe.com", + telephone:"1111111111" + }) + { + status + } +} +MUTATION; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage( + 'GraphQL response contains errors: Field ContactUsInput.name of required type String! was not provided.' + ); + $this->graphQlMutation($query, [], '', []); + } + + /** + * Failed send contact us form - missing name + */ + public function testContactUsMissingComment() + { + $query = <<<MUTATION +mutation { + contactUs(input: { + email:"test@adobe.com", + name:"John Doe", + telephone:"1111111111" + }) + { + status + } +} +MUTATION; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage( + 'GraphQL response contains errors: Field ContactUsInput.comment of required type String! was not provided.' + ); + $this->graphQlMutation($query, [], '', []); + } + + /** + * Failed send contact us form - missing name + */ + #[ + Config("contact/contact/enabled", "0") + ] + public function testContactUsDisabled() + { + $query = <<<MUTATION +mutation { + contactUs(input: { + comment:"Test Contact Us", + email:"test@adobe.com", + name:"John Doe", + telephone:"1111111111" + }) + { + status + } +} +MUTATION; + + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage('GraphQL response contains errors: The contact form is unavailable.'); + $this->graphQlMutation($query, [], '', []); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormCacheTest.php new file mode 100644 index 0000000000000..fedc5872bb802 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormCacheTest.php @@ -0,0 +1,686 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\ObjectManagerInterface; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\PageCache\Model\Config; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; + +/** + * Test caching for attributes form GraphQL query_CUSTOMER_REGISTER_ADDRESS. + */ +class AttributesFormCacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var AttributeInterface[] + */ + private $attributesToRemove; + + private const QUERY_CUSTOMER_REGISTER_ADDRESS = <<<QRY +{ + attributesForm(formCode: "customer_register_address") { + items { + code + } + errors { + type + message + } + } +} +QRY; + + private const QUERY_CUSTOMER_EDIT_ADDRESS = <<<QRY +{ + attributesForm(formCode: "customer_account_edit") { + items { + code + } + errors { + type + message + } + } +} +QRY; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->attributesToRemove = []; + parent::setUp(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + array_walk($this->attributesToRemove, function ($attribute) use ($eavAttributeRepo) { + $eavAttributeRepo->delete($attribute); + }); + parent::tearDown(); + } + + /** + * Obtains cache ID header from response + * + * @param string $query + * @return string + */ + private function getCacheIdHeader(string $query, array $headers = []): string + { + $response = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + $headers + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + return $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_1' + ) + ] + public function testAttributesFormCacheMissAndHit() + { + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + $cacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + + /** First response should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT and attribute should be present in a cached response */ + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + foreach ($response['body']['attributesForm']['items'] as $item) { + if (in_array($attribute1->getAttributeCode(), $item)) { + return; + } + } + $this->fail(sprintf( + "Attribute '%s' not found in query_CUSTOMER_REGISTER_ADDRESS response", + $attribute1->getAttributeCode() + )); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture( + StoreGroupFixture::class, + [ + 'website_id' => '$website2.id$' + ], + 'store_group2' + ), + DataFixture( + StoreFixture::class, + [ + 'store_group_id' => '$store_group2.id$' + ], + 'store2' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_1' + ) + ] + public function testAttributesFormCacheMissAndHitDifferentWebsites() + { + /** @var StoreInterface $store2 */ + $store2 = DataFixtureStorageManager::getStorage()->get('store2'); + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + $cacheIdStore1 = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore1] + ); + + $this->assertContains($attribute1->getAttributeCode(), array_map(function ($attribute) { + return $attribute['code']; + }, $response['body']['attributesForm']['items'])); + + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore1] + ); + + // obtain CacheID for Store 2 - has to be different than for Store 1: + $cacheIdStore2 = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS, ['Store' => $store2->getCode()]); + + // First query execution for a different store should result in a cache miss, while second one should be a hit + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [ + 'Store' => $store2->getCode(), + CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore2 + ] + ); + + $this->assertContains($attribute1->getAttributeCode(), array_map(function ($attribute) { + return $attribute['code']; + }, $response['body']['attributesForm']['items'])); + + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [ + 'Store' => $store2->getCode(), + CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore2 + ] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_1' + ) + ] + public function testAttributesFormCacheInvalidateOnAttributeEdit() + { + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + + $cacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + + /** First response should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT */ + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Modify attribute to invalidate cache */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $attribute1->setDefaultValue("default_value"); + $eavAttributeRepo->save($attribute1); + + /** Response after the change should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT */ + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + foreach ($response['body']['attributesForm']['items'] as $item) { + if (in_array($attribute1->getAttributeCode(), $item)) { + return; + } + } + $this->fail(sprintf( + "Attribute '%s' not found in query_CUSTOMER_REGISTER_ADDRESS response", + $attribute1->getAttributeCode() + )); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testAttributesFormCacheInvalidateOnAttributeCreate() + { + $cacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + + /** First response should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT */ + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Create new attribute and assign it to customer_register_address */ + $attributeCreate = $this->objectManager->get(CustomerAttribute::class); + $attribute = $attributeCreate->apply([ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ]); + $this->attributesToRemove[] = $attribute; + + /** Response after the creation of new attribute should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT */ + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + // verify created attribute is present in result + foreach ($response['body']['attributesForm']['items'] as $item) { + if (in_array($attribute->getAttributeCode(), $item)) { + return; + } + } + $this->fail(sprintf( + "Attribute '%s' not found in QUERY_CUSTOMER_REGISTER_ADDRESS response", + $attribute->getAttributeCode() + )); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_1' + ) + ] + public function testAttributesFormCacheInvalidateOnAttributeDelete() + { + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + $cacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + + /** First response should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Second response should be a HIT */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + /** Delete attribute to invalidate cache */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $deletedAttributeCode = $attribute1->getAttributeCode(); + $eavAttributeRepo->delete($attribute1); + + /** First response should be a MISS and attribute should NOT be present in a cached response */ + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + foreach ($response['body']['attributesForm']['items'] as $item) { + if (in_array($deletedAttributeCode, $item)) { + $this->fail(sprintf( + "Deleted attribute '%s' found in cached query_CUSTOMER_REGISTER_ADDRESS response", + $deletedAttributeCode + )); + } + } + + /** Second response should be a HIT */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + ], + 'attribute' + ) + ] + public function testAttributesFormCacheInvalidateOnAttributeAssignToForm() + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $queryEditCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + $queryRegisterCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_REGISTER_ADDRESS); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + /** Second response should be a HIT*/ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Assign $attribute to the 'customer_account_edit' form */ + $attribute->setData('used_in_forms', ['customer_account_edit']); + $eavAttributeRepo->save($attribute); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Non-affected "customer_register_address" form -> MISS, then cached and HIT */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + /** Add $attribute to the 'customer_register_address' form */ + $attribute->setData('used_in_forms', ['customer_account_edit', 'customer_register_address']); + $eavAttributeRepo->save($attribute); + + /** 'customer_register_address' form should be invalidated first now */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + /** Remove $attribute from the 'customer_account_edit' form */ + $attribute->setData('used_in_forms', ['customer_register_address']); + $eavAttributeRepo->save($attribute); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + /** Remove $attribute from remaining form(s) */ + $attribute->setData('used_in_forms', []); + $eavAttributeRepo->save($attribute); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => [ + 'customer_register_address', + 'customer_account_edit' + ] + ], + 'shared_attribute' + ) + ] + public function testAttributesFormCacheInvalidateOnDeletedSharedAttribute() + { + /** @var AttributeInterface $sharedAttribute */ + $sharedAttribute = DataFixtureStorageManager::getStorage()->get('shared_attribute'); + $queryEditCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + $queryRegisterCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_REGISTER_ADDRESS); + + /** First response should be a MISS from both queries */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + // /** Second response should be a HIT from both queries */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Delete attribute to invalidate both cached queries */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $deletedAttributeCode = $sharedAttribute->getAttributeCode(); + $eavAttributeRepo->delete($sharedAttribute); + + /** First response after deleting should be a MISS from both queries */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Second response should be a HIT from both queries as they are both cached back */ + $responseRegisterAddress = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $responseEditAddress = $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + // verify created attribute is NOT present in results + foreach ($responseRegisterAddress['body']['attributesForm']['items'] as $item) { + if (in_array($deletedAttributeCode, $item)) { + $this->fail( + sprintf( + "Attribute '%s' found in QUERY_CUSTOMER_REGISTER_ADDRESS response", + $deletedAttributeCode + ) + ); + } + } + + foreach ($responseEditAddress['body']['attributesForm']['items'] as $item) { + if (in_array($deletedAttributeCode, $item)) { + $this->fail( + sprintf( + "Attribute '%s' found in QUERY_CUSTOMER_EDIT_ADDRESS response", + $deletedAttributeCode + ) + ); + } + } + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => [ + 'customer_account_edit' + ] + ], + 'non_shared_attribute_2' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => [ + 'customer_register_address' + ] + ], + 'non_shared_attribute_1' + ) + ] + public function testAttributesFormCacheInvalidateOnDeletedNonSharedAttribute() + { + /** @var AttributeInterface $nonSharedAttribute1 */ + $nonSharedAttribute1 = DataFixtureStorageManager::getStorage()->get('non_shared_attribute_1'); + /** @var AttributeInterface $nonSharedAttribute2 */ + $nonSharedAttribute2 = DataFixtureStorageManager::getStorage()->get('non_shared_attribute_2'); + $queryEditCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_EDIT_ADDRESS); + $queryRegisterCacheId = $this->getCacheIdHeader(self::QUERY_CUSTOMER_REGISTER_ADDRESS); + + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + + /** First response should be a MISS from both queries */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Second response should be a HIT from all queries */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Delete nonSharedAttribute1 to invalidate cache of 'customer_register_address' ONLY*/ + $eavAttributeRepo->delete($nonSharedAttribute1); + + /** First response from QUERY_CUSTOMER_REGISTER_ADDRESS after deleting $nonSharedAttribute1 should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + /** other cached queries should not be affected */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Second response should be a HIT from all queries */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + + /** Delete nonSharedAttribute2 to invalidate cache of 'customer_account_edit' ONLY*/ + $eavAttributeRepo->delete($nonSharedAttribute2); + + /** First response from QUERY_CUSTOMER_EDIT_ADDRESS after deleting $nonSharedAttribute2 should be a MISS */ + $this->assertCacheMissAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + /** other cached queries should not be affected */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + + /** Second response should be a HIT from all queries */ + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_REGISTER_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryRegisterCacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_CUSTOMER_EDIT_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $queryEditCacheId] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php new file mode 100644 index 0000000000000..8a606179c5a3e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/AttributesFormTest.php @@ -0,0 +1,183 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test customer EAV form attributes metadata retrieval via GraphQL API + */ +class AttributesFormTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + attributesForm(formCode: "%s") { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_1' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_address_edit'] + ], + 'attribute_2' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'] + ], + 'attribute_3' + ) + ] + public function testAttributesForm(): void + { + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + /** @var AttributeInterface $attribute2 */ + $attribute2 = DataFixtureStorageManager::getStorage()->get('attribute_2'); + /** @var AttributeInterface $attribute3 */ + $attribute3 = DataFixtureStorageManager::getStorage()->get('attribute_3'); + $attribute3->setIsVisible(false)->save(); + + $result = $this->graphQlQuery(sprintf(self::QUERY, 'customer_register_address')); + + foreach ($result['attributesForm']['items'] as $item) { + if (array_contains($item, $attribute1->getAttributeCode())) { + return; + } + $this->assertNotContains($attribute2->getAttributeCode(), $item); + $this->assertNotContains($attribute3->getAttributeCode(), $item); + } + $this->fail(sprintf("Attribute '%s' not found in query response", $attribute1->getAttributeCode())); + } + + public function testAttributesFormAdminHtmlForm(): void + { + $this->assertEquals( + [ + 'attributesForm' => [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => 'Form "adminhtml_customer" could not be found.' + ] + ] + ] + ], + $this->graphQlQuery(sprintf(self::QUERY, 'adminhtml_customer')) + ); + } + + public function testAttributesFormDoesNotExist(): void + { + $this->assertEquals( + [ + 'attributesForm' => [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => 'Form "not_existing_form" could not be found.' + ] + ] + ] + ], + $this->graphQlQuery(sprintf(self::QUERY, 'not_existing_form')) + ); + } + + #[ + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'used_in_forms' => ['customer_register_address'], + 'website_id' => '$website2.id$', + 'scope_is_visible' => 1, + 'is_visible' => 0, + ], + 'attribute_1' + ), + ] + public function testAttributesFormScope(): void + { + /** @var AttributeInterface $attribute1 */ + $attribute1 = DataFixtureStorageManager::getStorage()->get('attribute_1'); + + $result = $this->graphQlQuery(sprintf(self::QUERY, 'customer_register_address')); + + foreach ($result['attributesForm']['items'] as $item) { + if (array_contains($item, $attribute1->getAttributeCode())) { + $this->fail( + sprintf("Attribute '%s' found in query response in global scope", $attribute1->getAttributeCode()) + ); + } + } + + /** @var StoreInterface $store */ + $store = DataFixtureStorageManager::getStorage()->get('store2'); + + $result = $this->graphQlQuery( + sprintf(self::QUERY, 'customer_register_address'), + [], + '', + ['Store' => $store->getCode()] + ); + + foreach ($result['attributesForm']['items'] as $item) { + if (array_contains($item, $attribute1->getAttributeCode())) { + return; + } + } + $this->fail( + sprintf( + "Attribute '%s' not found in query response in website scope", + $attribute1->getAttributeCode() + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/BooleanTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/BooleanTest.php new file mode 100644 index 0000000000000..b5775350b5def --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/BooleanTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class BooleanTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + options { + label + value + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'attribute' + ), + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'BOOLEAN', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'options' => [ + [ + 'label' => 'Yes', + 'value' => '1' + ], + [ + 'label' => 'No', + 'value' => '0' + ] + ] + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php new file mode 100644 index 0000000000000..a734054da00d9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/CustomerAddressAttributesTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class CustomerAddressAttributesTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + frontend_class + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + input_filter + validate_rules { + name + value + } + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'frontend_input' => 'date', + 'frontend_class' => 'hidden-for-virtual', + 'default_value' => '2023-03-22 00:00:00', + 'input_filter' => 'DATE', + 'validate_rules' => + '{"DATE_RANGE_MIN":"1679443200","DATE_RANGE_MAX":"1679875200","INPUT_VALIDATION":"DATE"}' + ], + 'attribute' + ), + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $formattedValidationRules = Bootstrap::getObjectManager()->get(FormatValidationRulesCommand::class)->execute( + $attribute->getValidationRules() + ); + + $result = $this->graphQlQuery( + sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer_address') + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER_ADDRESS', + 'frontend_input' => 'DATE', + 'frontend_class' => 'hidden-for-virtual', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'input_filter' => $attribute->getInputFilter(), + 'validate_rules' => $formattedValidationRules + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/DateTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/DateTest.php new file mode 100644 index 0000000000000..15cf97e6ee437 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/DateTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class DateTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + input_filter + validate_rules { + name + value + } + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'date', + 'default_value' => '2023-03-22 00:00:00', + 'input_filter' => 'DATE', + 'validate_rules' => ' + {"DATE_RANGE_MIN":"1679443200","DATE_RANGE_MAX":"1679875200","INPUT_VALIDATION":"DATE"} + ' + ], + 'attribute' + ), + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $formattedValidationRules = Bootstrap::getObjectManager()->get(FormatValidationRulesCommand::class)->execute( + $attribute->getValidationRules() + ); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'DATE', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'input_filter' => $attribute->getInputFilter(), + 'validate_rules' => $formattedValidationRules + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FileTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FileTest.php new file mode 100644 index 0000000000000..17e24e56a1c55 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FileTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class FileTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + validate_rules { + name + value + } + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'file', + 'validate_rules' => '{"MAX_FILE_SIZE":"10000000","FILE_EXTENSIONS":"PDF"}' + ], + 'attribute' + ) + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $formattedValidationRules = Bootstrap::getObjectManager()->get(FormatValidationRulesCommand::class)->execute( + $attribute->getValidationRules() + ); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'FILE', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'validate_rules' => $formattedValidationRules + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FormatValidationRulesCommand.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FormatValidationRulesCommand.php new file mode 100644 index 0000000000000..8a0891424a450 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/FormatValidationRulesCommand.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +/** + * Command to format validation rules appropriately for testing purposes. + */ +class FormatValidationRulesCommand +{ + /** + * Execute command to format validation rules provided as argument. + * + * @param array $validationRules + * @return array + */ + public function execute(array $validationRules): array + { + $formattedValidationRules = []; + foreach ($validationRules as $key => $value) { + $formattedValidationRules[] = ['name' => $key, 'value' => $value]; + } + return $formattedValidationRules; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/ImageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/ImageTest.php new file mode 100644 index 0000000000000..de98245af3ae9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/ImageTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class ImageTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + validate_rules { + name + value + } + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'image', + 'validate_rules' => '{"MAX_FILE_SIZE":"10000","MAX_IMAGE_WIDTH":"100"}' + ], + 'attribute' + ) + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $formattedValidationRules = Bootstrap::getObjectManager()->get(FormatValidationRulesCommand::class)->execute( + $attribute->getValidationRules() + ); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'IMAGE', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'validate_rules' => $formattedValidationRules + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultilineTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultilineTest.php new file mode 100644 index 0000000000000..35af3e83f3cbd --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultilineTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class MultilineTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + input_filter + multiline_count + sort_order + validate_rules { + name + value + } + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'multiline', + 'default_value' => 'this is line one +this is line two', + 'input_filter' => 'STRIPTAGS', + 'multiline_count' => 2, + 'sort_order' => 3, + 'validate_rules' => '{"MIN_TEXT_LENGTH":"100","MAX_TEXT_LENGTH":"200","INPUT_VALIDATION":"EMAIL"}', + ], + 'attribute' + ) + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $formattedValidationRules = Bootstrap::getObjectManager()->get(FormatValidationRulesCommand::class)->execute( + $attribute->getValidationRules() + ); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'MULTILINE', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'input_filter' => $attribute->getInputFilter(), + 'multiline_count' => $attribute->getMultilineCount(), + 'sort_order' => $attribute->getSortOrder(), + 'validate_rules' => $formattedValidationRules + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultiselectTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultiselectTest.php new file mode 100644 index 0000000000000..57b0761908a6f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/MultiselectTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\Eav\Test\Fixture\AttributeOption; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class MultiselectTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + default_value + options { + label + value + is_default + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'multiselect', + 'source_model' => Table::class + ], + 'attribute' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 10 + ], + 'option1' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 20, + 'is_default' => true + ], + 'option2' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 30, + 'is_default' => true + ], + 'option3' + ), + ] + public function testMetadata(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + /** @var AttributeOptionInterface $option1 */ + $option1 = DataFixtureStorageManager::getStorage()->get('option1'); + /** @var AttributeOptionInterface $option2 */ + $option2 = DataFixtureStorageManager::getStorage()->get('option2'); + /** @var AttributeOptionInterface $option3 */ + $option3 = DataFixtureStorageManager::getStorage()->get('option3'); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'default_value' => $option3->getValue() . ',' . $option2->getValue(), + 'options' => [ + $this->getOptionData($option1), + $this->getOptionData($option2), + $this->getOptionData($option3) + ] + ] + ], + 'errors' => [] + ] + ], + $result + ); + + $this->assertEquals($option2->getIsDefault(), true); + $this->assertEquals($option3->getIsDefault(), true); + } + + /** + * @param AttributeOptionInterface $option + * @return array + */ + private function getOptionData(AttributeOptionInterface $option): array + { + return [ + 'label' => $option->getLabel(), + 'value' => $option->getValue(), + 'is_default' => $option->getIsDefault() + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/SelectTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/SelectTest.php new file mode 100644 index 0000000000000..1e8c7dd338c2d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/SelectTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\Eav\Test\Fixture\AttributeOption; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class SelectTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + options { + label + value + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'select' + ], + 'attribute' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 10 + ], + 'option1' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 20 + ], + 'option2' + ), + ] + public function testMetadata(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + /** @var AttributeOptionInterface $option1 */ + $option1 = DataFixtureStorageManager::getStorage()->get('option1'); + /** @var AttributeOptionInterface $option2 */ + $option2 = DataFixtureStorageManager::getStorage()->get('option2'); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'options' => [ + $option1->getData(), + $option2->getData() + ] + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/StoreViewOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/StoreViewOptionsTest.php new file mode 100644 index 0000000000000..9229c7ba0f026 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/StoreViewOptionsTest.php @@ -0,0 +1,252 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\Eav\Test\Fixture\AttributeOption; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Test\Fixture\Store; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test customer EAV attribute options retrieval via GraphQL API + */ +#[ + DataFixture(Store::class, as: 'store_1'), + DataFixture(Store::class, as: 'store_2'), + DataFixture( + Attribute::class, + [ + 'frontend_labels' => [ + [ + 'store_id' => 0, + 'label' => 'height' + ], + [ + 'store_id' => '$store_1.id$', + 'label' => 'hair' + ], + [ + 'store_id' => '$store_2.id$', + 'label' => 'eyes' + ], + ], + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'select' + ], + 'attribute' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 10, + 'store_labels' => [ + [ + 'store_id' => 0, + 'label' => 'tall' + ], + [ + 'store_id' => '$store_1.id$', + 'label' => 'red' + ], + [ + 'store_id' => '$store_2.id$', + 'label' => 'green' + ] + ], + ], + 'option1' + ), + DataFixture( + AttributeOption::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$attribute.attribute_code$', + 'sort_order' => 20, + 'store_labels' => [ + [ + 'store_id' => 0, + 'label' => 'short' + ], + [ + 'store_id' => '$store_1.id$', + 'label' => 'brown' + ], + [ + 'store_id' => '$store_2.id$', + 'label' => 'blue' + ] + ], + ], + 'option2' + ), +] +class StoreViewOptionsTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + options { + label + } + } + errors { + type + message + } + } +} +QRY; + + public function testAttributeLabelsNoStoreViews(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + /** @var AttributeOptionInterface $option1 */ + $option1 = DataFixtureStorageManager::getStorage()->get('option1'); + + /** @var AttributeOptionInterface $option2 */ + $option2 = DataFixtureStorageManager::getStorage()->get('option2'); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getDefaultFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'SELECT', + 'is_required' => false, + 'default_value' => '', + 'is_unique' => false, + 'options' => [ + [ + 'label' => $option1->getLabel() + ], + [ + 'label' => $option2->getLabel() + ], + ] + ] + ], + 'errors' => [] + ] + ], + $this->graphQlQuery( + sprintf( + self::QUERY, + $attribute->getAttributeCode(), + 'customer' + ) + ) + ); + } + + public function testAttributeLabelsMultipleStoreViews(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + /** @var StoreInterface $store1 */ + $store1 = DataFixtureStorageManager::getStorage()->get('store_1'); + + /** @var StoreInterface $store2 */ + $store2 = DataFixtureStorageManager::getStorage()->get('store_2'); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel($store1->getId()), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'SELECT', + 'is_required' => false, + 'default_value' => '', + 'is_unique' => false, + 'options' => [ + [ + 'label' => 'red' + ], + [ + 'label' => 'brown' + ], + ] + ] + ], + 'errors' => [] + ] + ], + $this->graphQlQuery( + sprintf( + self::QUERY, + $attribute->getAttributeCode(), + 'customer' + ), + [], + '', + ['Store' => $store1->getCode()] + ) + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel($store2->getId()), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'SELECT', + 'is_required' => false, + 'default_value' => '', + 'is_unique' => false, + 'options' => [ + [ + 'label' => 'green' + ], + [ + 'label' => 'blue' + ], + ] + ] + ], + 'errors' => [] + ] + ], + $this->graphQlQuery( + sprintf( + self::QUERY, + $attribute->getAttributeCode(), + 'customer' + ), + [], + '', + ['Store' => $store2->getCode()] + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextTest.php new file mode 100644 index 0000000000000..5c348eb85f16c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class TextTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER + ], + 'attribute' + ) + ] + public function testTextField(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getDefaultFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'TEXT', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false + ] + ], + 'errors' => [] + ] + ], + $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')) + ); + } + + public function testErrorEntityNotFound(): void + { + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => 'Entity "non_existing_entity_type" could not be found.' + ] + ] + ] + ], + $this->graphQlQuery( + sprintf( + self::QUERY, + 'lastname', + 'non_existing_entity_type' + ) + ) + ); + } + + public function testErrorAttributeNotFound(): void + { + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ATTRIBUTE_NOT_FOUND', + 'message' => 'Attribute code "non_existing_code" could not be found.' + ] + ] + ] + ], + $this->graphQlQuery( + sprintf( + self::QUERY, + 'non_existing_code', + 'customer' + ) + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextareaTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextareaTest.php new file mode 100644 index 0000000000000..ac6383819b926 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/Attribute/TextareaTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer\Attribute; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +class TextareaTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "%s"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ... on CustomerAttributeMetadata { + input_filter + sort_order + } + } + errors { + type + message + } + } +} +QRY; + + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'textarea', + 'default_value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus bibendum finibus' . + 'quam, at vulputate quam feugiat tincidunt. Pellentesque venenatis nunc eget dolor' . + 'dictum, vel ultricies orci facilisis. Sed hendrerit arcu tristique dui molestie, ' . + 'sit amet scelerisque nibh scelerisque. Nulla sed tellus eget tellus volutpat ' . + 'vestibulum. Mauris molestie erat sed odio maximus accumsan. Morbi velit felis, ' . + 'tristique et lectus sollicitudin, laoreet aliquam nisl. Suspendisse vel ante at ' . + 'metus mattis ultrices non nec libero. Cras odio nunc, eleifend vitae interdum a, ' . + 'porttitor a dolor. Praesent mi odio, hendrerit quis consequat nec, vestibulum ' . + 'vitae justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin auctor' . + 'ac quam id rhoncus. Proin vel orci eu justo cursus vestibulum.', + 'input_filter' => 'ESCAPEHTML', + 'sort_order' => 4, + ], + 'attribute' + ) + ] + public function testMetadata(): void + { + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $result = $this->graphQlQuery(sprintf(self::QUERY, $attribute->getAttributeCode(), 'customer')); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontendLabel(), + 'entity_type' => 'CUSTOMER', + 'frontend_input' => 'TEXTAREA', + 'is_required' => false, + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => false, + 'input_filter' => $attribute->getInputFilter(), + 'sort_order' => $attribute->getSortOrder(), + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php index ed2b5245a2251..6f6e8bf185ed9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php @@ -72,6 +72,11 @@ public function testChangePassword() $response = $this->graphQlMutation($query, [], '', $headerMap); $this->assertEquals($customerEmail, $response['changeCustomerPassword']['email']); + $this->assertEmpty( + $response['changeCustomerPassword']['custom_attributes'], + 'custom_attributes should not contain any static values' + ); + try { // registry contains the old password hash so needs to be reset $this->customerRegistry->removeByEmail($customerEmail); @@ -216,6 +221,18 @@ private function getQuery($currentPassword, $newPassword) email firstname lastname + custom_attributes { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + value + label + } + } + } } } QUERY; 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 2b88621b407b4..ef33438f7acf0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -811,8 +811,8 @@ public function invalidInputDataProvider() { return [ ['', 'Syntax Error: Expected Name, found )'], - ['input: ""', 'requires type CustomerAddressInput!, found "".'], - ['input: "foo"', 'requires type CustomerAddressInput!, found "foo".'] + ['input: ""', 'Expected value of type "CustomerAddressInput", found "".'], + ['input: "foo"', 'Expected value of type "CustomerAddressInput", found "foo".'] ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressWithCustomAttributesV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressWithCustomAttributesV2Test.php new file mode 100644 index 0000000000000..31c27812cbb59 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressWithCustomAttributesV2Test.php @@ -0,0 +1,404 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for create customer address with custom attributes V2 + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_group_id' => 1, + 'attribute_code' => 'simple_attribute', + 'sort_order' => 2 + ], + 'simple_attribute', + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_group_id' => 1, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'multiselect_attribute', + 'frontend_input' => 'multiselect', + 'sort_order' => 1 + ], + 'multiselect_attribute', + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 1', + 'sort_order' => 20 + ], + 'multiselect_attribute_option1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 2', + 'sort_order' => 30 + ], + 'multiselect_attribute_option2' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 3', + 'sort_order' => 10 + ], + 'multiselect_attribute_option3' + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + ], + 'customer' + ) +] +class CreateCustomerAddressWithCustomAttributesV2Test extends GraphQlAbstract +{ + /** + * @var string + */ + private $currentPassword = 'password'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var AttributeMetadataInterface|null + */ + private $simple_attribute; + + /** + * @var AttributeMetadataInterface|null + */ + private $multiselect_attribute; + + /** + * @var AttributeOptionInterface|null + */ + private $option2; + + /** + * @var AttributeOptionInterface|null + */ + private $option3; + + /** + * @var CustomerInterface|null + */ + private $customer; + + /** + * @return void + * @throws LocalizedException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + $this->simple_attribute = DataFixtureStorageManager::getStorage()->get('simple_attribute'); + $this->multiselect_attribute = DataFixtureStorageManager::getStorage()->get('multiselect_attribute'); + $this->option2 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option2'); + $this->option3 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option3'); + $this->customer = DataFixtureStorageManager::getStorage()->get('customer'); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddressWithCustomAttributesV2() + { + $query = <<<QUERY +mutation { + createCustomerAddress(input: { + region: { + region_id: 4 + region: "Arizona" + region_code: "AZ" + } + country_code: US + street: ["123 Main Street"] + telephone: "7777777777" + postcode: "77777" + city: "Phoenix" + firstname: "Bob" + lastname: "Loblaw" + default_shipping: true + default_billing: false + custom_attributesV2: [ + { + attribute_code: "%s" + value: "%s" + }, + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + region { + region + region_code + } + country_code + street + telephone + postcode + city + default_shipping + default_billing + custom_attributesV2 { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + value + label + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation( + sprintf( + $query, + $this->simple_attribute->getAttributeCode(), + "brand new customer address value", + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'createCustomerAddress' => + [ + 'region' => [ + 'region' => 'Arizona', + 'region_code' => 'AZ' + ], + 'country_code' => 'US', + 'street' => [ + '123 Main Street' + ], + 'telephone' => '7777777777', + 'postcode' => '77777', + 'city' => 'Phoenix', + 'default_shipping' => true, + 'default_billing' => false, + 'custom_attributesV2' => + [ + 0 => + [ + 'code' => $this->multiselect_attribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->option3->getLabel(), + 'value' => $this->option3->getValue() + ], + [ + 'label' => $this->option2->getLabel(), + 'value' => $this->option2->getValue() + ] + ] + ], + 1 => + [ + 'code' => $this->simple_attribute->getAttributeCode(), + 'value' => 'brand new customer address value' + ] + ], + ], + ], + $response + ); + } + + /** + * @return void + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAttemptToCreateCustomerAddressPassingNonExistingOption() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Attribute multiselect_attribute does not contain option with Id 1345"); + + $query = <<<QUERY +mutation { + createCustomerAddress(input: { + region: { + region_id: 4 + region: "Arizona" + region_code: "AZ" + } + country_code: US + street: ["123 Main Street"] + telephone: "7777777777" + postcode: "77777" + city: "Phoenix" + firstname: "Bob" + lastname: "Loblaw" + default_shipping: true + default_billing: false + custom_attributesV2: [ + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + custom_attributesV2 { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + value + label + } + } + } + } +} +QUERY; + + $this->graphQlMutation( + sprintf( + $query, + $this->multiselect_attribute->getAttributeCode(), + "1345" + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @return void + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAttemptToCreateCustomerAddressPassingSelectedOptionsToDeprecatedCustomAttributes() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage( + "Field \"selected_options\" is not defined by type \"CustomerAddressAttributeInput\"" + ); + + $query = <<<QUERY +mutation { + createCustomerAddress(input: { + region: { + region_id: 4 + region: "Arizona" + region_code: "AZ" + } + country_code: US + street: ["123 Main Street"] + telephone: "7777777777" + postcode: "77777" + city: "Phoenix" + firstname: "Bob" + lastname: "Loblaw" + default_shipping: true + default_billing: false + custom_attributes: [ + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + custom_attributes { + attribute_code + value + selected_options { + value + label + } + } + } +} +QUERY; + $this->graphQlMutation( + sprintf( + $query, + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} 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 4f2b8f7566d31..8d605fa942495 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -234,7 +234,7 @@ public function invalidEmailAddressDataProvider(): array public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "test123" is not defined by type CustomerInput.'); + $this->expectExceptionMessage('Field "test123" is not defined by type "CustomerInput".'); $newFirstname = 'Richard'; $newLastname = 'Rowe'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php index df1533822424a..2ffc33e1c3b8b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php @@ -36,6 +36,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store newsletter/general/active 1 * @throws \Exception */ public function testCreateCustomerAccountWithPassword() @@ -117,7 +118,7 @@ public function testCreateCustomerAccountWithoutPassword() public function testCreateCustomerIfInputDataIsEmpty() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('CustomerCreateInput.email of required type String! was not provided.'); + $this->expectExceptionMessageMatches('/of required type String! was not provided./'); $query = <<<QUERY mutation { @@ -234,7 +235,7 @@ public function invalidEmailAddressDataProvider(): array public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "test123" is not defined by type CustomerCreateInput.'); + $this->expectExceptionMessage('Field "test123" is not defined by type "CustomerCreateInput".'); $newFirstname = 'Richard'; $newLastname = 'Rowe'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2WithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2WithCustomAttributesTest.php new file mode 100644 index 0000000000000..1e2a69277ca42 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2WithCustomAttributesTest.php @@ -0,0 +1,299 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; + +/** + * Tests for create customer V2 + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'attribute_code' => 'random_attribute', + 'sort_order' => 2 + ], + 'random_attribute', + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'multiselect_attribute', + 'frontend_input' => 'multiselect', + 'sort_order' => 1 + ], + 'multiselect_attribute', + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 1', + 'sort_order' => 20 + ], + 'multiselect_attribute_option1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'option 2', + 'sort_order' => 30 + ], + 'multiselect_attribute_option2' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'option 3', + 'sort_order' => 10 + ], + 'multiselect_attribute_option3' + ) +] +class CreateCustomerV2WithCustomAttributesTest extends GraphQlAbstract +{ + /** + * @var string + */ + private $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "Adam" + lastname: "Smith" + email: "adam@smith.com" + password: "test123#" + custom_attributes: [ + { + attribute_code: "%s" + value: "%s" + } + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + } + ) { + customer { + firstname + lastname + email + custom_attributes { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } +} +QUERY; + + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var AttributeMetadataInterface|null + */ + private $random_attribute; + + /** + * @var AttributeMetadataInterface|null + */ + private $multiselect_attribute; + + /** + * @var AttributeOptionInterface|null + */ + private $option2; + + /** + * @var AttributeOptionInterface|null + */ + private $option3; + + protected function setUp(): void + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + + $this->random_attribute = DataFixtureStorageManager::getStorage()->get('random_attribute'); + $this->multiselect_attribute = DataFixtureStorageManager::getStorage()->get('multiselect_attribute'); + $this->option2 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option2'); + $this->option3 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option3'); + } + + /** + * @return void + * @throws Exception + */ + public function testCreateCustomerAccountWithCustomAttributes() + { + $response = $this->graphQlMutation( + sprintf( + $this->query, + $this->random_attribute->getAttributeCode(), + 'new_value_for_attribute', + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ) + ); + + $this->assertEquals( + [ + 'createCustomerV2' => + [ + 'customer' => + [ + 'firstname' => 'Adam', + 'lastname' => 'Smith', + 'email' => 'adam@smith.com', + 'custom_attributes' => + [ + 0 => + [ + 'code' => $this->multiselect_attribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->option3->getLabel(), + 'value' => $this->option3->getValue() + ], + [ + 'label' => $this->option2->getLabel(), + 'value' => $this->option2->getValue() + ] + ] + ], + 1 => + [ + 'code' => $this->random_attribute->getAttributeCode(), + 'value' => 'new_value_for_attribute', + ] + ], + ], + ], + ], + $response + ); + } + + public function testCreateCustomerAccountWithNonExistingCustomAttribute() + { + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "John" + lastname: "Doe" + email: "john@doe.com" + password: "test123#" + custom_attributes: [ + { + attribute_code: "non_existing_custom_attribute", + value: "void" + } + ] + } + ) { + customer { + firstname + lastname + email + custom_attributes { + code + ... on AttributeValue { + value + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + $this->assertEquals( + [ + 'createCustomerV2' => + [ + 'customer' => + [ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'john@doe.com', + 'custom_attributes' => [] + ], + ], + ], + $response + ); + } + + protected function tearDown(): void + { + $email1 = 'adam@smith.com'; + $email2 = 'john@doe.com'; + try { + $customer1 = $this->customerRepository->get($email1); + $customer2 = $this->customerRepository->get($email2); + } catch (Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer1); + $this->customerRepository->delete($customer2); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php index 88da6ddee198a..ce9b0259215f7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php @@ -241,7 +241,7 @@ public function testDeleteCustomerAddressIfAccountIsLocked() $this->expectException(\Exception::class); $this->expectExceptionMessage('The account is locked'); - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/750'); + $this->markTestSkipped('https://github.com/magento/graphql-ce/issues/750'); $userName = 'customer@example.com'; $password = 'password'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesWithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesWithCustomAttributesTest.php new file mode 100644 index 0000000000000..b968549fcf75e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesWithCustomAttributesTest.php @@ -0,0 +1,325 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * GraphQl tests for @see \Magento\CustomerGraphQl\Model\Customer\GetCustomer. + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'frontend_input' => 'multiselect', + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'labels', + 'attribute_group_id' => 1, + 'sort_order' => 2, + ], + 'multiselect_customer_address_attribute' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => 'planet', + 'sort_order' => 1, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_group_id' => 1, + ], + 'varchar_customer_address_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_customer_address_attribute.attribute_code$', + 'label' => 'far', + 'sort_order' => 20 + ], + 'multiselect_customer_address_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_customer_address_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'foreign', + 'is_default' => true + ], + 'multiselect_customer_address_attribute_option_2' + ), + DataFixture( + Customer::class, + [ + 'email' => 'john@doe.com', + 'addresses' => [ + [ + 'country_id' => 'US', + 'region_id' => 32, + 'city' => 'Boston', + 'street' => ['10 Milk Street'], + 'postcode' => '02108', + 'telephone' => '1234567890', + 'default_billing' => true, + 'default_shipping' => true, + 'custom_attributes' => [ + [ + 'attribute_code' => 'labels', + 'selected_options' => [ + ['value' => '$multiselect_customer_address_attribute_option_1.value$'], + ['value' => '$multiselect_customer_address_attribute_option_2.value$'] + ], + ], + [ + 'attribute_code' => 'planet', + 'value' => 'Earth' + ] + ], + ], + ], + ], + 'customer' + ), +] +class GetAddressesWithCustomAttributesTest extends GraphQlAbstract +{ + /** + * @var string + */ + private $currentPassword = 'password'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var AttributeInterface|null + */ + private $varcharCustomerAddressAttribute; + + /** + * @var AttributeInterface|null + */ + private $multiselectCustomerAddressAttribute; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomerAttributeOption1; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomerAttributeOption2; + + /** + * @var CustomerInterface|null + */ + private $customer; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + + $this->varcharCustomerAddressAttribute = DataFixtureStorageManager::getStorage()->get( + 'varchar_customer_address_attribute' + ); + $this->multiselectCustomerAddressAttribute = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_address_attribute' + ); + $this->multiselectCustomerAttributeOption1 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_address_attribute_option_1' + ); + $this->multiselectCustomerAttributeOption2 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_address_attribute_option_2' + ); + $this->customer = DataFixtureStorageManager::getStorage()->get('customer'); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + public function testGetCustomAddressAttributes() + { + $query = <<<QUERY +query { + customer { + firstname + lastname + email + addresses { + country_id + custom_attributesV2 { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'customer' => [ + 'firstname' => $this->customer->getFirstname(), + 'lastname' => $this->customer->getLastname(), + 'email' => $this->customer->getEmail(), + 'addresses' => [ + [ + 'country_id' => 'US', + 'custom_attributesV2' => [ + [ + 'code' => $this->varcharCustomerAddressAttribute->getAttributeCode(), + 'value' => 'Earth' + ], + [ + 'code' => $this->multiselectCustomerAddressAttribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->multiselectCustomerAttributeOption2->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption2->getValue(), + ], + [ + 'label' => $this->multiselectCustomerAttributeOption1->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption1->getValue(), + ] + ] + ] + ] + ] + ] + ] + ], + $response + ); + } + + public function testGetFilteredCustomAddressAttributes() + { + $query = <<<QUERY +query { + customer { + firstname + lastname + email + addresses { + country_id + custom_attributesV2(attributeCodes: ["%s"]) { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + sprintf($query, $this->multiselectCustomerAddressAttribute->getAttributeCode()), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'customer' => [ + 'firstname' => $this->customer->getFirstname(), + 'lastname' => $this->customer->getLastname(), + 'email' => $this->customer->getEmail(), + 'addresses' => [ + [ + 'country_id' => 'US', + 'custom_attributesV2' => [ + [ + 'code' => $this->multiselectCustomerAddressAttribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->multiselectCustomerAttributeOption2->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption2->getValue(), + ], + [ + 'label' => $this->multiselectCustomerAttributeOption1->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption1->getValue(), + ] + ] + ] + ] + ] + ] + ] + ], + $response + ); + } +} 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 5fc316c0d46f3..9bc4a3f358681 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php @@ -12,11 +12,13 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\CustomerAuthUpdate; use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Bootstrap as TestBootstrap; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -48,7 +50,6 @@ class GetCustomerTest extends GraphQlAbstract * @var ObjectManagerInterface */ private $objectManager; - /** * @inheridoc */ @@ -205,6 +206,7 @@ public function testAccountIsNotConfirmed() * @param string $email * @param string $password * @return array + * @throws AuthenticationException */ private function getCustomerAuthHeaders(string $email, string $password): array { @@ -216,6 +218,7 @@ private function getCustomerAuthHeaders(string $email, string $password): array /** * @param int $customerId * @return void + * @throws NoSuchEntityException */ private function lockCustomer(int $customerId): void { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerWithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerWithCustomAttributesTest.php new file mode 100644 index 0000000000000..17b3857739b48 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerWithCustomAttributesTest.php @@ -0,0 +1,297 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * GraphQl tests for @see \Magento\CustomerGraphQl\Model\Customer\GetCustomer. + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => 'shoe_size', + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'sort_order' => 2 + ], + 'varchar_customer_attribute' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'multiselect', + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'shoe_color', + 'attribute_group_id' => 1, + 'sort_order' => 1 + ], + 'multiselect_customer_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_customer_attribute.attribute_code$', + 'label' => 'red', + 'sort_order' => 20 + ], + 'multiselect_customer_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_customer_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'white', + 'is_default' => true + ], + 'multiselect_customer_attribute_option_2' + ), + DataFixture( + Customer::class, + [ + 'email' => 'john@doe.com', + 'custom_attributes' => [ + [ + 'attribute_code' => 'shoe_size', + 'value' => '42' + ], + [ + 'attribute_code' => 'shoe_color', + 'selected_options' => [ + ['value' => '$multiselect_customer_attribute_option_1.value$'], + ['value' => '$multiselect_customer_attribute_option_2.value$'] + ], + ], + ], + ], + 'customer' + ), +] +class GetCustomerWithCustomAttributesTest extends GraphQlAbstract +{ + /** + * @var string + */ + private $currentPassword = 'password'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var AttributeInterface|null + */ + private $varcharCustomerAttribute; + + /** + * @var AttributeInterface|null + */ + private $multiselectCustomerAttribute; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomerAttributeOption1; + + /** + * @var AttributeOptionInterface|null + */ + private $multiselectCustomerAttributeOption2; + + /** + * @var CustomerInterface|null + */ + private $customer; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + + $this->varcharCustomerAttribute = DataFixtureStorageManager::getStorage()->get( + 'varchar_customer_attribute' + ); + $this->multiselectCustomerAttribute = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_attribute' + ); + $this->multiselectCustomerAttributeOption1 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_attribute_option_1' + ); + $this->multiselectCustomerAttributeOption2 = DataFixtureStorageManager::getStorage()->get( + 'multiselect_customer_attribute_option_2' + ); + $this->customer = DataFixtureStorageManager::getStorage()->get('customer'); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + public function testGetCustomAttributes() + { + $query = <<<QUERY +query { + customer { + firstname + lastname + email + custom_attributes { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'customer' => [ + 'firstname' => $this->customer->getFirstname(), + 'lastname' => $this->customer->getLastname(), + 'email' => $this->customer->getEmail(), + 'custom_attributes' => [ + [ + 'code' => $this->multiselectCustomerAttribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->multiselectCustomerAttributeOption2->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption2->getValue(), + ], + [ + 'label' => $this->multiselectCustomerAttributeOption1->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption1->getValue(), + ] + ] + ], + [ + 'code' => $this->varcharCustomerAttribute->getAttributeCode(), + 'value' => '42' + ] + ] + ] + ], + $response + ); + } + + public function testGetFilteredCustomAttributes() + { + $query = <<<QUERY +query { + customer { + firstname + lastname + email + custom_attributes(attributeCodes: ["%s"]) { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + sprintf($query, $this->multiselectCustomerAttribute->getAttributeCode()), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'customer' => [ + 'firstname' => $this->customer->getFirstname(), + 'lastname' => $this->customer->getLastname(), + 'email' => $this->customer->getEmail(), + 'custom_attributes' => [ + [ + 'code' => $this->multiselectCustomerAttribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->multiselectCustomerAttributeOption2->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption2->getValue(), + ], + [ + 'label' => $this->multiselectCustomerAttributeOption1->getLabel(), + 'value' => $this->multiselectCustomerAttributeOption1->getValue(), + ] + ] + ] + ] + ] + ], + $response + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php index f0bd7dc1e854a..b683b4bd521d6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php @@ -7,6 +7,11 @@ namespace Magento\GraphQl\Customer; +use Magento\Customer\Model\AccountManagement; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -14,6 +19,25 @@ */ class IsEmailAvailableTest extends GraphQlAbstract { + /** + * @var ScopeConfigInterface|null + */ + private ?ScopeConfigInterface $scopeConfig; + + /** + * @var string|null + */ + private ?string $storeId; + + public function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + /* @var StoreResolverInterface $storeResolver */ + $storeResolver = $objectManager->get(StoreResolverInterface::class); + $this->storeId = $storeResolver->getCurrentStoreId(); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ @@ -31,7 +55,16 @@ public function testEmailNotAvailable() self::assertArrayHasKey('isEmailAvailable', $response); self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']); - self::assertFalse($response['isEmailAvailable']['is_email_available']); + $emailConfig = $this->scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_STORE, + $this->storeId + ); + if (!$emailConfig) { + self::assertTrue($response['isEmailAvailable']['is_email_available']); + } else { + self::assertFalse($response['isEmailAvailable']['is_email_available']); + } } /** 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 7b32600e74aa7..5fcadc21f43a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -388,7 +388,7 @@ public function testUpdateCustomerAddressWithInvalidIdType() MUTATION; $this->expectException(Exception::class); - $this->expectExceptionMessage('Field "updateCustomerAddress" argument "id" requires type Int!, found "".'); + $this->expectExceptionMessage('Int cannot represent non-integer value: ""'); $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } @@ -430,9 +430,9 @@ public function invalidInputDataProvider() ['', '"input" value must be specified'], [ 'input: ""', - 'Field "updateCustomerAddress" argument "input" requires type CustomerAddressInput, found ""' + 'Expected value of type "CustomerAddressInput", found ""' ], - ['input: "foo"', 'requires type CustomerAddressInput, found "foo"'] + ['input: "foo"', 'Expected value of type "CustomerAddressInput", found "foo"'] ]; } @@ -481,7 +481,7 @@ public function testUpdateCustomerAddressIfAccountIsLocked() $this->expectException(\Exception::class); $this->expectExceptionMessage('The account is locked.'); - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/750'); + $this->markTestSkipped('https://github.com/magento/graphql-ce/issues/750'); $userName = 'customer@example.com'; $password = 'password'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressWithCustomAttributesV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressWithCustomAttributesV2Test.php new file mode 100644 index 0000000000000..f454965804d35 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressWithCustomAttributesV2Test.php @@ -0,0 +1,379 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for update customer address with custom attributes V2 + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_group_id' => 1, + 'attribute_code' => 'simple_attribute', + 'sort_order' => 2 + ], + 'simple_attribute', + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_set_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_group_id' => 1, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'multiselect_attribute', + 'frontend_input' => 'multiselect', + 'sort_order' => 1 + ], + 'multiselect_attribute', + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 1', + 'sort_order' => 20 + ], + 'multiselect_attribute_option1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 2', + 'sort_order' => 30 + ], + 'multiselect_attribute_option2' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 3', + 'sort_order' => 10 + ], + 'multiselect_attribute_option3' + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'addresses' => [ + [ + 'country_id' => 'US', + 'region_id' => 32, + 'city' => 'Boston', + 'street' => ['10 Milk Street'], + 'postcode' => '02108', + 'telephone' => '1234567890', + 'default_billing' => true, + 'default_shipping' => true, + 'custom_attributes' => [ + [ + 'attribute_code' => '$simple_attribute.attribute_code$', + 'value' => 'value_one' + ], + [ + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'selected_options' => [ + [ + 'value' => '$multiselect_attribute_option1.value$' + ], + [ + 'value' => '$multiselect_attribute_option2.value$' + ] + ] + ] + ] + ] + ] + ], + 'customer' + ) +] +class UpdateCustomerAddressWithCustomAttributesV2Test extends GraphQlAbstract +{ + /** + * @var string + */ + private $currentPassword = 'password'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var AttributeMetadataInterface|null + */ + private $simple_attribute; + + /** + * @var AttributeMetadataInterface|null + */ + private $multiselect_attribute; + + /** + * @var AttributeOptionInterface|null + */ + private $option2; + + /** + * @var AttributeOptionInterface|null + */ + private $option3; + + /** + * @var CustomerInterface|null + */ + private $customer; + + /** + * @var AddressInterface|null + */ + private $customerAddress; + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + $this->simple_attribute = DataFixtureStorageManager::getStorage()->get('simple_attribute'); + $this->multiselect_attribute = DataFixtureStorageManager::getStorage()->get('multiselect_attribute'); + $this->option2 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option2'); + $this->option3 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option3'); + $this->customer = DataFixtureStorageManager::getStorage()->get('customer'); + + $customerAddresses = $this->customer->getAddresses(); + $this->customerAddress = array_shift($customerAddresses); + } + + /** + * @return void + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testUpdateCustomerAddressWithCustomAttributesV2() + { + $query = <<<QUERY +mutation { + updateCustomerAddress(id: "%s", input: { + custom_attributesV2: [ + { + attribute_code: "%s" + value: "%s" + }, + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + custom_attributesV2 { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation( + sprintf( + $query, + $this->customerAddress->getId(), + $this->simple_attribute->getAttributeCode(), + "another simple value", + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'updateCustomerAddress' => + [ + 'custom_attributesV2' => + [ + 0 => + [ + 'code' => $this->multiselect_attribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->option3->getLabel(), + 'value' => $this->option3->getValue() + ], + [ + 'label' => $this->option2->getLabel(), + 'value' => $this->option2->getValue() + ] + ] + ], + 1 => + [ + 'code' => $this->simple_attribute->getAttributeCode(), + 'value' => 'another simple value' + ] + ], + ], + ], + $response + ); + } + + /** + * @return void + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAttemptToUpdateCustomerAddressPassingNonExistingOption() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Attribute multiselect_attribute does not contain option with Id 1345"); + + $query = <<<QUERY +mutation { + updateCustomerAddress(id: "%s", input: { + custom_attributesV2: [ + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + custom_attributesV2 { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + value + label + } + } + } + } +} +QUERY; + + $this->graphQlMutation( + sprintf( + $query, + $this->customerAddress->getId(), + $this->multiselect_attribute->getAttributeCode(), + "1345" + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @return void + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAttemptToUpdateCustomerAddressPassingSelectedOptionsToDeprecatedCustomAttributes() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage( + "Field \"selected_options\" is not defined by type \"CustomerAddressAttributeInput\"" + ); + + $query = <<<QUERY +mutation { + updateCustomerAddress(id: "%s", input: { + custom_attributes: [ + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + }) { + custom_attributes { + attribute_code + value + selected_options { + value + label + } + } + } +} +QUERY; + + $this->graphQlMutation( + sprintf( + $query, + $this->customerAddress->getId(), + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2WithCustomAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2WithCustomAttributesTest.php new file mode 100644 index 0000000000000..1e42c0245bd26 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2WithCustomAttributesTest.php @@ -0,0 +1,529 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend; +use Magento\Eav\Model\Entity\Attribute\Source\Table; +use Magento\Eav\Test\Fixture\AttributeOption as AttributeOptionFixture; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for update customer V2 + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'attribute_code' => 'random_attribute', + 'sort_order' => 2 + ], + 'random_attribute', + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'multiselect_attribute', + 'frontend_input' => 'multiselect', + 'sort_order' => 1 + ], + 'multiselect_attribute', + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'line 1', + 'sort_order' => 20 + ], + 'multiselect_attribute_option1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'option 2', + 'sort_order' => 30 + ], + 'multiselect_attribute_option2' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'label' => 'option 3', + 'sort_order' => 10 + ], + 'multiselect_attribute_option3' + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'custom_attributes' => [ + [ + 'attribute_code' => '$random_attribute.attribute_code$', + 'value' => 'value_one' + ], + [ + 'attribute_code' => '$multiselect_attribute.attribute_code$', + 'selected_options' => [ + [ + 'value' => '$multiselect_attribute_option1.value$' + ], + [ + 'value' => '$multiselect_attribute_option2.value$' + ] + ] + ] + ] + ], + 'customer' + ) +] +class UpdateCustomerV2WithCustomAttributesTest extends GraphQlAbstract +{ + /** + * @var string + */ + private $simpleQuery = <<<QUERY +mutation { + updateCustomerV2( + input: { + custom_attributes: [ + { + attribute_code: "%s", + value: "%s" + } + ] + } + ) { + customer { + email + custom_attributes { + code + ... on AttributeValue { + value + } + } + } + } +} +QUERY; + + /** + * @var string + */ + private $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + custom_attributes: [ + { + attribute_code: "%s", + value: "%s" + } + { + attribute_code: "%s" + value: "%s" + selected_options: [] + } + ] + } + ) { + customer { + email + custom_attributes { + code + ... on AttributeValue { + value + } + ... on AttributeSelectedOptions { + selected_options { + label + value + } + } + } + } + } +} +QUERY; + + /** + * @var string + */ + private $currentPassword = 'password'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var AttributeMetadataInterface|null + */ + private $random_attribute; + + /** + * @var AttributeMetadataInterface|null + */ + private $multiselect_attribute; + + /** + * @var AttributeOptionInterface|null + */ + private $option2; + + /** + * @var AttributeOptionInterface|null + */ + private $option3; + + /** + * @var CustomerInterface|null + */ + private $customer; + + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + $this->random_attribute = DataFixtureStorageManager::getStorage()->get('random_attribute'); + $this->multiselect_attribute = DataFixtureStorageManager::getStorage()->get('multiselect_attribute'); + $this->option2 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option2'); + $this->option3 = DataFixtureStorageManager::getStorage()->get('multiselect_attribute_option3'); + $this->customer = DataFixtureStorageManager::getStorage()->get('customer'); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + */ + public function testUpdateCustomerWithCorrectCustomerAttribute(): void + { + $response = $this->graphQlMutation( + sprintf( + $this->query, + $this->random_attribute->getAttributeCode(), + 'new_value_for_attribute', + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($this->customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'updateCustomerV2' => + [ + 'customer' => + [ + 'email' => 'customer@example.com', + 'custom_attributes' => + [ + 0 => + [ + 'code' => $this->multiselect_attribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->option3->getLabel(), + 'value' => $this->option3->getValue() + ], + [ + 'label' => $this->option2->getLabel(), + 'value' => $this->option2->getValue() + ] + ] + ], + 1 => + [ + 'code' => $this->random_attribute->getAttributeCode(), + 'value' => 'new_value_for_attribute' + ] + ], + ], + ], + ], + $response + ); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + */ + public function testAttemptToUpdateCustomerPassingNonExistingCustomerAttribute(): void + { + /** @var CustomerInterface $customer */ + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + + $response = $this->graphQlMutation( + sprintf( + $this->query, + 'non_existing_custom_attribute', + 'new_value_for_attribute', + $this->multiselect_attribute->getAttributeCode(), + $this->option2->getValue() . "," . $this->option3->getValue() + ), + [], + '', + $this->getCustomerAuthHeaders($customer->getEmail(), $this->currentPassword) + ); + + $this->assertEquals( + [ + 'updateCustomerV2' => + [ + 'customer' => + [ + 'email' => 'customer@example.com', + 'custom_attributes' => + [ + 0 => + [ + 'code' => $this->multiselect_attribute->getAttributeCode(), + 'selected_options' => [ + [ + 'label' => $this->option3->getLabel(), + 'value' => $this->option3->getValue() + ], + [ + 'label' => $this->option2->getLabel(), + 'value' => $this->option2->getValue() + ] + ] + ], + 1 => + [ + 'code' => $this->random_attribute->getAttributeCode(), + 'value' => 'value_one' + ] + ], + ], + ], + ], + $response + ); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + * @throws Exception + */ + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'sort_order' => 1, + 'attribute_code' => 'date_attribute', + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'frontend_input' => 'date', + 'backend_type' => 'datetime', + 'input_filter' => 'date', + ], + 'date_attribute', + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'custom_attributes' => [ + [ + 'attribute_code' => '$date_attribute.attribute_code$', + 'value' => '2023-03-22 00:00:00' + ] + ] + ], + 'customer' + ) + ] + public function testAttemptToUpdateCustomerAttributeWithInvalidDataType(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Invalid date"); + + /** @var CustomerInterface $customer */ + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + + /** @var AttributeMetadataInterface $date_attribute */ + $date_attribute = DataFixtureStorageManager::getStorage()->get('date_attribute'); + + $this->graphQlMutation( + sprintf( + $this->simpleQuery, + $date_attribute->getAttributeCode(), + 'this_is_an_invalid_value_for_dates' + ), + [], + '', + $this->getCustomerAuthHeaders($customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + */ + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'attribute_code' => 'date_range_attribute', + 'frontend_input' => 'date', + 'frontend_class' => 'Magento\Eav\Model\Entity\Attribute\Frontend\Datetime', + 'backend_model' => 'Magento\Eav\Model\Entity\Attribute\Backend\Datetime', + 'backend_type' => 'datetime', + 'input_filter' => 'date', + 'validate_rules' => + '{"date_range_min":1679443200,"date_range_max":1679875200,"input_validation":"date"}' + ], + 'date_range_attribute', + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'custom_attributes' => [ + [ + 'attribute_code' => '$date_range_attribute.attribute_code$', + 'value' => '1679443200' + ] + ] + ], + 'customer' + ) + ] + public function testAttemptToUpdateCustomerAttributeWithInvalidValue(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Please enter a valid date between 22/03/2023 and 27/03/2023"); + + /** @var CustomerInterface $customer */ + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + + /** @var AttributeMetadataInterface $date_range */ + $date_range = DataFixtureStorageManager::getStorage()->get('date_range_attribute'); + + $this->graphQlMutation( + sprintf( + $this->simpleQuery, + $date_range->getAttributeCode(), + '1769443200' + ), + [], + '', + $this->getCustomerAuthHeaders($customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @return void + * @throws AuthenticationException + * @throws LocalizedException + */ + #[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'sort_order' => 1, + 'attribute_code' => 'boolean_attribute', + 'attribute_set_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'attribute_group_id' => 1, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'boolean_attribute', + ), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'custom_attributes' => [ + [ + 'attribute_code' => '$boolean_attribute.attribute_code$', + 'value' => '1' + ] + ] + ], + 'customer' + ) + ] + public function testAttemptToUpdateBooleanCustomerAttributeWithInvalidValue(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Attribute boolean_attribute does not contain option with Id 3"); + + /** @var CustomerInterface $customer */ + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + + /** @var AttributeMetadataInterface $date_attribute */ + $date_attribute = DataFixtureStorageManager::getStorage()->get('boolean_attribute'); + + $this->graphQlMutation( + sprintf( + $this->simpleQuery, + $date_attribute->getAttributeCode(), + "3" + ), + [], + '', + $this->getCustomerAuthHeaders($customer->getEmail(), $this->currentPassword) + ); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerGraphQl/Model/Resolver/CustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerGraphQl/Model/Resolver/CustomerTest.php new file mode 100644 index 0000000000000..b1f3fbc4fc43b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerGraphQl/Model/Resolver/CustomerTest.php @@ -0,0 +1,865 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CustomerGraphQl\Model\Resolver; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Test\Fixture\Customer as CustomerFixture; +use Magento\CustomerGraphQl\Model\Resolver\Customer as CustomerResolver; +use Magento\CustomerGraphQl\Model\Resolver\IsSubscribed; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\Newsletter\Model\SubscriptionManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResolverCacheAbstract; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; + +/** + * Test for customer resolver cache + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CustomerTest extends ResolverCacheAbstract +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var Registry + */ + private $registry; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + $this->graphQlResolverCache = $this->objectManager->get( + GraphQlResolverCache::class + ); + + $this->customerRepository = $this->objectManager->get( + CustomerRepositoryInterface::class + ); + + $this->websiteRepository = $this->objectManager->get( + WebsiteRepositoryInterface::class + ); + + // first register secure area so we have permission to delete customer in tests + $this->registry = $this->objectManager->get(Registry::class); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + parent::setUp(); + } + + protected function tearDown(): void + { + // reset secure area to false (was set to true in setUp so we could delete customer in tests) + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @param callable $invalidationMechanismCallable + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @dataProvider invalidationMechanismProvider + */ + public function testCustomerResolverCacheAndInvalidation(callable $invalidationMechanismCallable) + { + $customer = $this->customerRepository->get('customer@example.com'); + + $query = $this->getCustomerQuery(); + + $token = $this->generateCustomerToken($customer->getEmail(), 'password'); + + $this->mockCustomerUserInfoContext($customer); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + $this->assertCurrentCustomerCacheRecordExists($customer); + + // call query again to ensure no errors are thrown + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + // change customer data + $invalidationMechanismCallable($customer, $token); + // assert that cache entry is invalidated + $this->assertCurrentCustomerCacheRecordDoesNotExist(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + */ + public function testCustomerIsSubscribedResolverCacheAndInvalidation() + { + /** @var SubscriptionManagerInterface $subscriptionManager */ + $subscriptionManager = $this->objectManager->get(SubscriptionManagerInterface::class); + $customer = $this->customerRepository->get('customer@example.com'); + // unsubscribe customer to initialize state + $subscriptionManager->unsubscribeCustomer((int)$customer->getId(), (int)$customer->getStoreId()); + + $query = $this->getCustomerQuery(); + + $token = $this->generateCustomerToken($customer->getEmail(), 'password'); + + $this->mockCustomerUserInfoContext($customer); + $response = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + $this->assertFalse($response['body']['customer']['is_subscribed']); + $this->assertCurrentCustomerCacheRecordExists($customer); + $this->assertIsSubscribedRecordExists($customer, false); + + // call query again to ensure no errors are thrown + $response = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + $this->assertFalse($response['body']['customer']['is_subscribed']); + + // change customer subscription + $subscriptionManager->subscribeCustomer((int)$customer->getId(), (int)$customer->getStoreId()); + $this->assertIsSubscribedRecordNotExists($customer); + $this->assertCurrentCustomerCacheRecordExists($customer); + + // query customer again so that subscription cache record is created + $response = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + $this->assertTrue($response['body']['customer']['is_subscribed']); + + $this->assertIsSubscribedRecordExists($customer, true); + // unsubscribe customer to restore original state + $subscriptionManager->unsubscribeCustomer((int)$customer->getId(), (int)$customer->getStoreId()); + $this->assertIsSubscribedRecordNotExists($customer); + } + + /** + * Prepare cache key for subscription flag cache record. + * + * @param CustomerInterface $customer + * @return string + */ + private function getCacheKeyForIsSubscribedResolver(CustomerInterface $customer): string + { + $resolverMock = $this->getMockBuilder(IsSubscribed::class)->disableOriginalConstructor()->getMock(); + /** @var ProviderInterface $cacheKeyCalculatorProvider */ + $cacheKeyCalculatorProvider = Bootstrap::getObjectManager()->get(ProviderInterface::class); + $cacheKeyFactor = $cacheKeyCalculatorProvider + ->getKeyCalculatorForResolver($resolverMock) + ->calculateCacheKey( + ['model' => $customer] + ); + $cacheKeyQueryPayloadMetadata = IsSubscribed::class . '\Interceptor[]'; + $cacheKeyParts = [ + GraphQlResolverCache::CACHE_TAG, + $cacheKeyFactor, + sha1($cacheKeyQueryPayloadMetadata) + ]; + // strtoupper is called in \Magento\Framework\Cache\Frontend\Adapter\Zend::_unifyId + return strtoupper(implode('_', $cacheKeyParts)); + } + + /** + * Assert subscription cache record exists for the given customer. + * + * @param CustomerInterface $customer + * @param bool $expectedValue + * @return void + */ + private function assertIsSubscribedRecordExists(CustomerInterface $customer, bool $expectedValue) + { + $cacheKey = $this->getCacheKeyForIsSubscribedResolver($customer); + $cacheEntry = Bootstrap::getObjectManager()->get(GraphQlResolverCache::class)->load($cacheKey); + $this->assertIsString($cacheEntry); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEquals( + $expectedValue, + $cacheEntryDecoded + ); + } + + /** + * Assert subscription cache record does not exist for the given customer. + * + * @param CustomerInterface $customer + * @return void + */ + private function assertIsSubscribedRecordNotExists(CustomerInterface $customer) + { + $cacheKey = $this->getCacheKeyForIsSubscribedResolver($customer); + $cacheEntry = Bootstrap::getObjectManager()->get(GraphQlResolverCache::class)->load($cacheKey); + $this->assertFalse($cacheEntry); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + */ + public function testCustomerResolverCacheInvalidationOnStoreChange() + { + $customer = $this->customerRepository->get('customer@example.com'); + + $query = $this->getCustomerQuery(); + + $token = $this->generateCustomerToken($customer->getEmail(), 'password'); + + $this->mockCustomerUserInfoContext($customer); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + $this->assertCurrentCustomerCacheRecordExists($customer); + + // call query again to ensure no errors are thrown + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + // change customer data + $storeManager = Bootstrap::getObjectManager()->get( + StoreManagerInterface::class + ); + $secondStore = $storeManager->getStore('fixture_second_store'); + $customer->setStoreId($secondStore->getId()); + $this->customerRepository->save($customer); + // assert that cache entry is invalidated + $this->assertCurrentCustomerCacheRecordDoesNotExist(); + } + + /** + * Assert that cache record exists for the given customer. + * + * @param CustomerInterface $customer + * @return void + */ + private function assertCurrentCustomerCacheRecordExists(CustomerInterface $customer) + { + $cacheKey = $this->getCacheKeyForCustomerResolver(); + $cacheEntry = Bootstrap::getObjectManager()->get(GraphQlResolverCache::class)->load($cacheKey); + $cacheEntryDecoded = json_decode($cacheEntry, true); + + $this->assertEquals( + $customer->getEmail(), + $cacheEntryDecoded['email'] + ); + } + + /** + * Assert that cache record does not exist for the given customer. + * + * @return void + */ + private function assertCurrentCustomerCacheRecordDoesNotExist() + { + $cacheKey = $this->getCacheKeyForCustomerResolver(); + $this->assertFalse( + Bootstrap::getObjectManager()->get(GraphQlResolverCache::class)->test($cacheKey) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/two_customers.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @return void + */ + public function testCustomerResolverCacheGeneratesSeparateEntriesForEachCustomer() + { + $customer1 = $this->customerRepository->get('customer@example.com'); + $customer2 = $this->customerRepository->get('customer_two@example.com'); + + $query = $this->getCustomerQuery(); + + // query customer1 + $customer1Token = $this->generateCustomerToken( + $customer1->getEmail(), + 'password' + ); + + $this->mockCustomerUserInfoContext($customer1); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $customer1Token] + ); + + $customer1CacheKey = $this->getCacheKeyForCustomerResolver(); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($customer1CacheKey) + ); + + // query customer2 + $this->mockCustomerUserInfoContext($customer2); + $customer2Token = $this->generateCustomerToken( + $customer2->getEmail(), + 'password' + ); + + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $customer2Token] + ); + + $customer2CacheKey = $this->getCacheKeyForCustomerResolver(); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($customer2CacheKey) + ); + + $this->assertNotEquals( + $customer1CacheKey, + $customer2CacheKey + ); + + // change customer 1 and assert customer 2 cache entry is not invalidated + $customer1->setFirstname('NewFirstName'); + $this->customerRepository->save($customer1); + + $this->assertFalse( + $this->graphQlResolverCache->test($customer1CacheKey) + ); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($customer2CacheKey) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @return void + */ + public function testCustomerResolverCacheInvalidatesWhenCustomerGetsDeleted() + { + $customer = $this->customerRepository->get('customer@example.com'); + + $query = $this->getCustomerQuery(); + $token = $this->generateCustomerToken( + $customer->getEmail(), + 'password' + ); + + $this->mockCustomerUserInfoContext($customer); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + $cacheKey = $this->getCacheKeyForCustomerResolver(); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($cacheKey) + ); + + $this->assertTagsByCacheKeyAndCustomer($cacheKey, $customer); + + // delete customer and assert that cache entry is invalidated + $this->customerRepository->delete($customer); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @return void + */ + #[ + DataFixture(WebsiteFixture::class, ['code' => 'website2'], 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$', 'code' => 'store2'], 'store2'), + DataFixture( + CustomerFixture::class, + [ + 'firstname' => 'Customer1', + 'email' => 'same_email@example.com', + 'store_id' => '1' // default store + ] + ), + DataFixture( + CustomerFixture::class, + [ + 'firstname' => 'Customer2', + 'email' => 'same_email@example.com', + 'website_id' => '$website2.id$', + ] + ) + ] + public function testCustomerWithSameEmailInTwoSeparateWebsitesKeepsSeparateCacheEntries() + { + $website2 = $this->websiteRepository->get('website2'); + + $customer1 = $this->customerRepository->get('same_email@example.com'); + $customer2 = $this->customerRepository->get('same_email@example.com', $website2->getId()); + + $query = $this->getCustomerQuery(); + + // query customer1 + $customer1Token = $this->generateCustomerToken( + $customer1->getEmail(), + 'password' + ); + + $this->mockCustomerUserInfoContext($customer1); + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $customer1Token] + ); + + $customer1CacheKey = $this->getCacheKeyForCustomerResolver(); + $customer1CacheEntry = $this->graphQlResolverCache->load($customer1CacheKey); + $customer1CacheEntryDecoded = json_decode($customer1CacheEntry, true); + $this->assertEquals( + $customer1->getFirstname(), + $customer1CacheEntryDecoded['firstname'] + ); + + // query customer2 + $this->mockCustomerUserInfoContext($customer2); + $customer2Token = $this->generateCustomerToken( + $customer2->getEmail(), + 'password', + 'store2' + ); + + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + [ + 'Authorization' => 'Bearer ' . $customer2Token, + 'Store' => 'store2', + ] + ); + + $customer2CacheKey = $this->getCacheKeyForCustomerResolver(); + + $customer2CacheEntry = $this->graphQlResolverCache->load($customer2CacheKey); + $customer2CacheEntryDecoded = json_decode($customer2CacheEntry, true); + $this->assertEquals( + $customer2->getFirstname(), + $customer2CacheEntryDecoded['firstname'] + ); + + // change customer 1 and assert customer 2 cache entry is not invalidated + $customer1->setFirstname('NewFirstName'); + $this->customerRepository->save($customer1); + + $this->assertFalse( + $this->graphQlResolverCache->test($customer1CacheKey) + ); + + $this->assertIsNumeric( + $this->graphQlResolverCache->test($customer2CacheKey) + ); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @return void + */ + public function testGuestQueryingCustomerDoesNotGenerateResolverCacheEntry() + { + $query = $this->getCustomerQuery(); + + try { + $this->graphQlQueryWithResponseHeaders( + $query + ); + $this->fail('Expected exception not thrown'); + } catch (ResponseContainsErrorsException $e) { + // expected exception + } + + $cacheKey = $this->getCacheKeyForCustomerResolver(); + + $this->assertFalse( + $this->graphQlResolverCache->test($cacheKey) + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testCustomerQueryingCustomerWithDifferentStoreHeaderDoesNotGenerateResolverCacheEntry() + { + $customer = $this->customerRepository->get('customer@example.com'); + + $query = $this->getCustomerQuery(); + $token = $this->generateCustomerToken( + $customer->getEmail(), + 'password' + ); + + $lowLevelFrontendCache = $this->graphQlResolverCache->getLowLevelFrontend(); + + $originalTagCount = count( + $lowLevelFrontendCache->getIdsMatchingTags([$this->graphQlResolverCache::CACHE_TAG]) + ); + + $this->mockCustomerUserInfoContext($customer); + + // query customer with default store header + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Authorization' => 'Bearer ' . $token] + ); + + $tagCountAfterQueryingInDefaultStore = count( + $lowLevelFrontendCache->getIdsMatchingTags([$this->graphQlResolverCache::CACHE_TAG]) + ); + + $this->assertGreaterThan( + $originalTagCount, + $tagCountAfterQueryingInDefaultStore + ); + + // query customer with second store header + $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + [ + 'Authorization' => 'Bearer ' . $token, + 'Store' => 'fixture_second_store', + ] + ); + + $tagCountAfterQueryingInSecondStore = count( + $lowLevelFrontendCache->getIdsMatchingTags([$this->graphQlResolverCache::CACHE_TAG]) + ); + + // if tag count after second store query is same as after default store query, no new tags have been created + // and we can assume no separate cache entry has been generated + $this->assertEquals( + $tagCountAfterQueryingInDefaultStore, + $tagCountAfterQueryingInSecondStore + ); + } + + public function invalidationMechanismProvider(): array + { + // provider is invoked before setUp() is called so need to init here + $repo = Bootstrap::getObjectManager()->get( + CustomerRepositoryInterface::class + ); + return [ + 'change firstname' => [ + function (CustomerInterface $customer) use ($repo) { + $customer->setFirstname('SomeNewFirstName'); + $repo->save($customer); + }, + ], + 'change is_subscribed' => [ + function (CustomerInterface $customer) use ($repo) { + $isCustomerSubscribed = $customer->getExtensionAttributes()->getIsSubscribed(); + $customer->getExtensionAttributes()->setIsSubscribed(!$isCustomerSubscribed); + $repo->save($customer); + }, + ], + 'add and delete address' => [ + function (CustomerInterface $customer, $tokenString) { + // create new address because default billing address cannot be deleted + $this->graphQlMutation( + $this->getCreateAddressMutation("4000 Polk St"), + [], + '', + ['Authorization' => 'Bearer ' . $tokenString] + ); + // query for customer to cache data after address creation + $result = $this->graphQlQuery( + $this->getCustomerQuery(), + [], + '', + ['Authorization' => 'Bearer ' . $tokenString] + ); + // assert that cache record exists for given customer + $this->assertCurrentCustomerCacheRecordExists($customer); + + $addressId = $result['customer']['addresses'][1]['id']; + $result = $this->graphQlMutation( + $this->getDeleteAddressMutation($addressId), + [], + '', + ['Authorization' => 'Bearer ' . $tokenString] + ); + $this->assertTrue($result['deleteCustomerAddress']); + }, + ], + 'update address' => [ + function (CustomerInterface $customer, $tokenString) { + // query for customer to cache data after address creation + $result = $this->graphQlQuery( + $this->getCustomerQuery(), + [], + '', + ['Authorization' => 'Bearer ' . $tokenString] + ); + + $addressId = $result['customer']['addresses'][0]['id']; + $result = $this->graphQlMutation( + $this->getUpdateAddressStreetMutation($addressId, "8000 New St"), + [], + '', + ['Authorization' => 'Bearer ' . $tokenString] + ); + $this->assertEquals($addressId, $result['updateCustomerAddress']['id']); + $this->assertEquals("8000 New St", $result['updateCustomerAddress']['street'][0]); + }, + ], + ]; + } + + /** + * @param string $streetAddress + * @return string + */ + private function getCreateAddressMutation($streetAddress) + { + return <<<MUTATIONCREATE +mutation{ + createCustomerAddress(input: { + city: "Houston", + company: "Customer Company", + country_code: US, + fax: "12341234567", + firstname: "User", + lastname: "Lastname", + postcode: "77023", + region: { + region_code: "TX", + region_id: 57 + }, + street: ["{$streetAddress}"], + telephone: "12340987654" + }) { + city + country_code + firstname + id + lastname + postcode + region_id + street + telephone + } +} +MUTATIONCREATE; + } + + /** + * @param int $addressId + * @param string $streetAddress + * @return string + */ + private function getUpdateAddressStreetMutation($addressId, $streetAddress) + { + return <<<MUTATIONUPDATE +mutation{ + updateCustomerAddress( + id: {$addressId} + input: { + street: ["{$streetAddress}"] + } + ) { + id + street + } +} +MUTATIONUPDATE; + } + + /** + * @param int $addressId + * @return string + */ + private function getDeleteAddressMutation($addressId) + { + return <<<MUTATIONDELETE +mutation{ + deleteCustomerAddress(id: {$addressId}) +} +MUTATIONDELETE; + } + + private function assertTagsByCacheKeyAndCustomer(string $cacheKey, CustomerInterface $customer): void + { + $lowLevelFrontendCache = $this->graphQlResolverCache->getLowLevelFrontend(); + $cacheIdPrefix = $lowLevelFrontendCache->getOption('cache_id_prefix'); + $metadatas = $lowLevelFrontendCache->getMetadatas($cacheKey); + $tags = $metadatas['tags']; + + $this->assertEqualsCanonicalizing( + [ + $cacheIdPrefix . strtoupper(Customer::ENTITY) . '_' . $customer->getId(), + $cacheIdPrefix . strtoupper(GraphQlResolverCache::CACHE_TAG), + $cacheIdPrefix . 'MAGE', + ], + $tags + ); + } + + private function getCacheKeyForCustomerResolver(): string + { + $resolverMock = $this->getMockBuilder(CustomerResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var ProviderInterface $cacheKeyCalculatorProvider */ + $cacheKeyCalculatorProvider = Bootstrap::getObjectManager()->get(ProviderInterface::class); + + $cacheKeyFactor = $cacheKeyCalculatorProvider + ->getKeyCalculatorForResolver($resolverMock) + ->calculateCacheKey(); + + $cacheKeyQueryPayloadMetadata = CustomerResolver::class . '\Interceptor[]'; + + $cacheKeyParts = [ + GraphQlResolverCache::CACHE_TAG, + $cacheKeyFactor, + sha1($cacheKeyQueryPayloadMetadata) + ]; + + // strtoupper is called in \Magento\Framework\Cache\Frontend\Adapter\Zend::_unifyId + return strtoupper(implode('_', $cacheKeyParts)); + } + + private function getCustomerQuery(): string + { + return <<<QUERY + { + customer { + id + firstname + lastname + email + is_subscribed + addresses { + id + street + city + region { + region + } + postcode + } + } + } + QUERY; + } + + /** + * Generate customer token + * + * @param string $email + * @param string $password + * @param string $storeCode + * @return string + * @throws \Exception + */ + private function generateCustomerToken(string $email, string $password, string $storeCode = 'default'): string + { + $query = <<<MUTATION +mutation { + generateCustomerToken( + email: "{$email}" + password: "{$password}" + ) { + token + } +} +MUTATION; + + $response = $this->graphQlMutation( + $query, + [], + '', + [ + 'Store' => $storeCode, + ] + ); + + return $response['generateCustomerToken']['token']; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesCacheTest.php new file mode 100644 index 0000000000000..c36208f28a73f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesCacheTest.php @@ -0,0 +1,566 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\Store\Model\Group; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\Website; +use Magento\TestFramework\App\ApiMutableScopeConfig; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test Countries query cache + */ +class CountriesCacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ApiMutableScopeConfig + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var array + */ + private $origConfigs = []; + + /** + * @var array + */ + private $notExistingOrigConfigs = []; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->config = $this->objectManager->get(ApiMutableScopeConfig::class); + } + + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + */ + public function testGetCountries() + { + // Query default store countries + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($this->getQuery()); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery(), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('countries', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['countries']; + $this->assertCount(1, $defaultStoreResponseResult); + $this->assertEquals('US', $defaultStoreResponseResult[0]['id']); + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery(), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('countries', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['countries']; + $this->assertCount(1, $defaultStoreResponseHitResult); + $this->assertEquals('US', $defaultStoreResponseHitResult[0]['id']); + + // Query test store countries + $testStoreCode = 'test'; + $responseTestStore = $this->graphQlQueryWithResponseHeaders( + $this->getQuery(), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStore['headers']); + $testStoreCacheId = $responseTestStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($testStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $testStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery(), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('countries', $testStoreResponse['body']); + $testStoreResponseResult = $testStoreResponse['body']['countries']; + $this->assertCount(2, $testStoreResponseResult); + // Verify we obtain a cache HIT at the 2nd time + $testStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery(), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('countries', $testStoreResponseHit['body']); + $testStoreResponseHitResult = $testStoreResponseHit['body']['countries']; + $this->assertCount(2, $testStoreResponseHitResult); + } + + /** + * Store scoped country config change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + */ + public function testCachePurgedWithStoreScopeCountryConfigChange() + { + // Query default store countries + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($this->getQuery()); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery(), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('countries', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['countries']; + $this->assertCount(1, $defaultStoreResponseResult); + $this->assertEquals('US', $defaultStoreResponseResult[0]['id']); + + // Query test store countries + $testStoreCode = 'test'; + $responseTestStore = $this->graphQlQueryWithResponseHeaders( + $this->getQuery(), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStore['headers']); + $testStoreCacheId = $responseTestStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($testStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $testStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery(), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('countries', $testStoreResponse['body']); + $testStoreResponseResult = $testStoreResponse['body']['countries']; + $this->assertCount(2, $testStoreResponseResult); + + // Change test store allowed country + $this->setConfig('general/country/allow', 'DE', ScopeInterface::SCOPE_STORE, $testStoreCode); + + // Query default store countries after test store country config is changed + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery(), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('countries', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['countries']; + $this->assertCount(1, $defaultStoreResponseHitResult); + $this->assertEquals('US', $defaultStoreResponseHitResult[0]['id']); + + // Query test store countries after test store country config is changed + // Verify we obtain a cache MISS at the 2nd time + $testStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $this->getQuery(), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('countries', $testStoreResponseMiss['body']); + $testStoreResponseMissResult = $testStoreResponseMiss['body']['countries']; + $this->assertCount(1, $testStoreResponseMissResult); + // Verify we obtain a cache HIT at the 3rd time + $testStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery(), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('countries', $testStoreResponseHit['body']); + $testStoreResponseHitResult = $testStoreResponseHit['body']['countries']; + $this->assertCount(1, $testStoreResponseHitResult); + } + + /** + * Website scope country config change triggers purging only the cache of the stores + * associated with the changed website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteScopeCountryConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query default store countries + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertCount(1, $secondStoreResponse['body']['countries']); + + // Query third store countries + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertCount(1, $thirdStoreResponse['body']['countries']); + + // Change second website allowed country + $this->setConfig('general/country/allow', 'US,DE', ScopeInterface::SCOPE_WEBSITES, 'second'); + + // Query default store countries after the country config of the second website is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries after the country config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertCount(2, $secondStoreResponseMiss['body']['countries']); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store countries after the country config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $thirdStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertCount(2, $thirdStoreResponseMiss['body']['countries']); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Default scope country config change triggers purging the cache of all stores. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - third - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithDefaultScopeCountryConfigChange(): void + { + $query = $this->getQuery(); + + // Query default store countries + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertCount(1, $secondStoreResponse['body']['countries']); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertCount(1, $thirdStoreResponse['body']['countries']); + + // Change default allowed country + $this->setConfig('general/country/allow', 'US,DE', ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + + // Query default store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $defaultStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertCount(2, $defaultStoreResponseMiss['body']['countries']); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertCount(2, $secondStoreResponseMiss['body']['countries']); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $thirdStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertCount(2, $thirdStoreResponseMiss['body']['countries']); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + private function changeToTwoWebsitesThreeStoreGroupsThreeStores() + { + /** @var $website2 \Magento\Store\Model\Website */ + $website2 = $this->objectManager->create(Website::class); + $website2Id = $website2->load('second', 'code')->getId(); + + // Change third store to the same website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3GroupId = $store3->getStoreGroupId(); + /** @var Group $store3Group */ + $store3Group = $this->objectManager->create(Group::class); + $store3Group->load($store3GroupId)->setWebsiteId($website2Id)->save(); + $store3->setWebsiteId($website2Id)->save(); + } + + /** + * Get query + * + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +query { + countries { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + } + + protected function tearDown(): void + { + $this->restoreConfig(); + parent::tearDown(); + } + + /** + * Set configuration + * + * @param string $path + * @param string $value + * @param string $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + string $path, + string $value, + string $scopeType, + ?string $scopeCode = null + ): void { + if ($this->configStorage->checkIsRecordExist($path, $scopeType, $scopeCode)) { + $this->origConfigs[] = [ + 'path' => $path, + 'value' => $this->configStorage->getValueFromDb($path, $scopeType, $scopeCode), + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } else { + $this->notExistingOrigConfigs[] = [ + 'path' => $path, + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } + $this->config->setValue($path, $value, $scopeType, $scopeCode); + } + + private function restoreConfig() + { + foreach ($this->origConfigs as $origConfig) { + $this->config->setValue( + $origConfig['path'], + $origConfig['value'], + $origConfig['scopeType'], + $origConfig['scopeCode'] + ); + } + $this->origConfigs = []; + + foreach ($this->notExistingOrigConfigs as $notExistingOrigConfig) { + $this->configStorage->deleteConfigFromDb( + $notExistingOrigConfig['path'], + $notExistingOrigConfig['scopeType'], + $notExistingOrigConfig['scopeCode'] + ); + } + $this->notExistingOrigConfigs = []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php index 09db8564f9e0a..15f1c4d12c855 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php @@ -10,42 +10,61 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the GraphQL endpoint's Coutries query + * Test the GraphQL endpoint's Countries query */ class CountriesTest extends GraphQlAbstract { + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + */ public function testGetCountries() { - $query = <<<QUERY -query { - countries { - id - two_letter_abbreviation - three_letter_abbreviation - full_name_locale - full_name_english - available_regions { - id - code - name - } - } -} -QUERY; - - $result = $this->graphQlQuery($query); + $result = $this->graphQlQuery($this->getQuery()); $this->assertArrayHasKey('countries', $result); + $this->assertCount(1, $result['countries']); $this->assertArrayHasKey('id', $result['countries'][0]); $this->assertArrayHasKey('two_letter_abbreviation', $result['countries'][0]); $this->assertArrayHasKey('three_letter_abbreviation', $result['countries'][0]); $this->assertArrayHasKey('full_name_locale', $result['countries'][0]); $this->assertArrayHasKey('full_name_english', $result['countries'][0]); $this->assertArrayHasKey('available_regions', $result['countries'][0]); + + $testStoreResult = $this->graphQlQuery( + $this->getQuery(), + [], + '', + ['Store' => 'test'] + ); + $this->assertArrayHasKey('countries', $testStoreResult); + $this->assertCount(2, $testStoreResult['countries']); } public function testCheckCountriesForNullLocale() { - $query = <<<QUERY + $result = $this->graphQlQuery($this->getQuery()); + $count = count($result['countries']); + for ($i=0; $i < $count; $i++) { + $this->assertNotNull($result['countries'][$i]['full_name_locale']); + } + } + + /** + * Get query + * + * @return string + */ + private function getQuery(): string + { + return <<<QUERY query { countries { id @@ -61,11 +80,5 @@ public function testCheckCountriesForNullLocale() } } QUERY; - - $result = $this->graphQlQuery($query); - $count = count($result['countries']); - for ($i=0; $i < $count; $i++) { - $this->assertNotNull($result['countries'][$i]['full_name_locale']); - } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryCacheTest.php new file mode 100644 index 0000000000000..447bcd19cc359 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryCacheTest.php @@ -0,0 +1,703 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\Store\Model\Group; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\Website; +use Magento\TestFramework\App\ApiMutableScopeConfig; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test Country query cache + */ +class CountryCacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ApiMutableScopeConfig + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var array + */ + private $origConfigs = []; + + /** + * @var array + */ + private $notExistingOrigConfigs = []; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->config = $this->objectManager->get(ApiMutableScopeConfig::class); + } + + /** + * Country query is cached + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetCountry() + { + // Query default store US country + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($this->getQuery('US')); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('US'), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('country', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['country']; + $this->assertEquals('US', $defaultStoreResponseResult['id']); + $this->assertEquals('US', $defaultStoreResponseResult['two_letter_abbreviation']); + $this->assertEquals('USA', $defaultStoreResponseResult['three_letter_abbreviation']); + $this->assertEquals('United States', $defaultStoreResponseResult['full_name_locale']); + $this->assertEquals('United States', $defaultStoreResponseResult['full_name_english']); + $this->assertCount(65, $defaultStoreResponseResult['available_regions']); + $this->assertArrayHasKey('id', $defaultStoreResponseResult['available_regions'][0]); + $this->assertArrayHasKey('code', $defaultStoreResponseResult['available_regions'][0]); + $this->assertArrayHasKey('name', $defaultStoreResponseResult['available_regions'][0]); + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('US'), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('country', $defaultStoreResponse['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['country']; + $this->assertEquals('US', $defaultStoreResponseHitResult['id']); + $this->assertEquals('US', $defaultStoreResponseHitResult['two_letter_abbreviation']); + $this->assertEquals('USA', $defaultStoreResponseHitResult['three_letter_abbreviation']); + $this->assertEquals('United States', $defaultStoreResponseHitResult['full_name_locale']); + $this->assertEquals('United States', $defaultStoreResponseHitResult['full_name_english']); + $this->assertCount(65, $defaultStoreResponseHitResult['available_regions']); + $this->assertArrayHasKey('id', $defaultStoreResponseHitResult['available_regions'][0]); + $this->assertArrayHasKey('code', $defaultStoreResponseHitResult['available_regions'][0]); + $this->assertArrayHasKey('name', $defaultStoreResponseHitResult['available_regions'][0]); + + // Query test store US country + $testStoreCode = 'test'; + $responseTestStoreUsCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('US'), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStoreUsCountry['headers']); + $testStoreUsCountryCacheId = $responseTestStoreUsCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($testStoreUsCountryCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $testStoreUsCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('US'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreUsCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreUsCountryResponse['body']); + $testStoreUsCountryResponseResult = $testStoreUsCountryResponse['body']['country']; + $this->assertEquals('US', $testStoreUsCountryResponseResult['id']); + // Verify we obtain a cache HIT at the 2nd time + $testStoreUsCountryResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('US'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreUsCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreUsCountryResponseHit['body']); + $testStoreUsCountryResponseHitResult = $testStoreUsCountryResponseHit['body']['country']; + $this->assertEquals('US', $testStoreUsCountryResponseHitResult['id']); + + // Query test store DE country + $responseTestStoreDeCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('DE'), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStoreDeCountry['headers']); + $testStoreDeCountryCacheId = $responseTestStoreDeCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $testStoreDeCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreDeCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreDeCountryResponse['body']); + $testStoreDeCountryResponseResult = $testStoreDeCountryResponse['body']['country']; + $this->assertEquals('DE', $testStoreDeCountryResponseResult['id']); + $this->assertCount(16, $testStoreDeCountryResponseResult['available_regions']); + // Verify we obtain a cache HIT at the 2nd time + $testStoreDeCountryResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreDeCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreDeCountryResponseHit['body']); + $testStoreDeCountryResponseHitResult = $testStoreDeCountryResponseHit['body']['country']; + $this->assertEquals('DE', $testStoreDeCountryResponseHitResult['id']); + $this->assertCount(16, $testStoreDeCountryResponseHitResult['available_regions']); + } + + /** + * Store scoped country config change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreScopeCountryConfigChange() + { + // Query default store US country + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($this->getQuery('US')); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('US'), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('country', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['country']; + $this->assertEquals('US', $defaultStoreResponseResult['id']); + + // Query test store US country + $testStoreCode = 'test'; + $responseTestStoreUsCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('US'), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStoreUsCountry['headers']); + $testStoreCacheId = $responseTestStoreUsCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($testStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $testStoreUsCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery("US"), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreUsCountryResponse['body']); + $testStoreUsCountryResponseResult = $testStoreUsCountryResponse['body']['country']; + $this->assertEquals('US', $testStoreUsCountryResponseResult['id']); + + // Query test store DE country + $responseTestStoreDeCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('DE'), + [], + '', + ['Store' => $testStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStoreDeCountry['headers']); + $testStoreDeCountryCacheId = $responseTestStoreDeCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $testStoreDeCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery("DE"), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreDeCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreDeCountryResponse['body']); + $testStoreDeCountryResponseResult = $testStoreDeCountryResponse['body']['country']; + $this->assertEquals('DE', $testStoreDeCountryResponseResult['id']); + + // Change test store allowed country + $this->setConfig('general/country/allow', 'DE', ScopeInterface::SCOPE_STORE, $testStoreCode); + + // Query default store countries after test store country config is changed + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('US'), + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('country', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['country']; + $this->assertEquals('US', $defaultStoreResponseHitResult['id']); + + // Query test store DE country after test store country config is changed + // Verify we obtain a cache MISS at the 2nd time + $testStoreDeCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery("DE"), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreDeCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreDeCountryResponse['body']); + $testStoreDeCountryResponseResult = $testStoreDeCountryResponse['body']['country']; + $this->assertEquals('DE', $testStoreDeCountryResponseResult['id']); + // Verify we obtain a cache HIT at the 3rd time + $testStoreDeCountryResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery("DE"), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreDeCountryCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('country', $testStoreDeCountryResponseHit['body']); + $testStoreDeCountryResponseHitResult = $testStoreDeCountryResponseHit['body']['country']; + $this->assertEquals('DE', $testStoreDeCountryResponseHitResult['id']); + + // Query test store US country after test store country config is changed + // Verify we obtain a cache MISS at the 2nd time + $this->expectException(\Exception::class); + $this->expectExceptionMessage('GraphQL response contains errors: The country isn\'t available.'); + $this->assertCacheMissAndReturnResponse( + $this->getQuery('US'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + } + + /** + * Website scope country config change triggers purging only the cache of the stores + * associated with the changed website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteScopeCountryConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery('US'); + + // Query default store US country + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store US country + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store US country + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Change second website allowed country + $this->setConfig('general/country/allow', 'US,DE', ScopeInterface::SCOPE_WEBSITES, 'second'); + + // Query default store countries after the country config of the second website is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store US country after the country config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query second store DE country after the country config of its associated second website is changed + $responseSecondStoreDeCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('DE'), + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreDeCountryCacheId = $responseSecondStoreDeCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time, the cache is purged + $secondStoreDeCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreDeCountryCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('country', $secondStoreDeCountryResponse['body']); + $secondStoreDeCountryResponseResult = $secondStoreDeCountryResponse['body']['country']; + $this->assertEquals('DE', $secondStoreDeCountryResponseResult['id']); + // Verify we obtain a cache HIT at the 2nd time + $secondStoreDeCountryResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreDeCountryCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('country', $secondStoreDeCountryResponseHit['body']); + $secondStoreDeCountryResponseHitResult = $secondStoreDeCountryResponseHit['body']['country']; + $this->assertEquals('DE', $secondStoreDeCountryResponseHitResult['id']); + + // Query third store US country after the country config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query third store DE country after the country config of its associated second website is changed + $responseThirdStoreDeCountry = $this->graphQlQueryWithResponseHeaders( + $this->getQuery('DE'), + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreDeCountryCacheId = $responseThirdStoreDeCountry['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time, the cache is purged + $thirdStoreDeCountryResponse = $this->assertCacheMissAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreDeCountryCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertArrayHasKey('country', $thirdStoreDeCountryResponse['body']); + $thirdStoreDeCountryResponseResult = $thirdStoreDeCountryResponse['body']['country']; + $this->assertEquals('DE', $thirdStoreDeCountryResponseResult['id']); + // Verify we obtain a cache HIT at the 2nd time + $thirdStoreDeCountryResponseHit = $this->assertCacheHitAndReturnResponse( + $this->getQuery('DE'), + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreDeCountryCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertArrayHasKey('country', $thirdStoreDeCountryResponseHit['body']); + $thirdStoreDeCountryResponseHitResult = $thirdStoreDeCountryResponseHit['body']['country']; + $this->assertEquals('DE', $thirdStoreDeCountryResponseHitResult['id']); + } + + /** + * Default scope country config change triggers purging the cache of all stores. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - third - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithDefaultScopeCountryConfigChange(): void + { + $query = $this->getQuery('US'); + + // Query default store countries + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Change default allowed country + $this->setConfig('general/country/allow', 'US,DE', ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + + // Query default store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store countries after the default country config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + private function changeToTwoWebsitesThreeStoreGroupsThreeStores() + { + /** @var $website2 \Magento\Store\Model\Website */ + $website2 = $this->objectManager->create(Website::class); + $website2Id = $website2->load('second', 'code')->getId(); + + // Change third store to the same website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3GroupId = $store3->getStoreGroupId(); + /** @var Group $store3Group */ + $store3Group = $this->objectManager->create(Group::class); + $store3Group->load($store3GroupId)->setWebsiteId($website2Id)->save(); + $store3->setWebsiteId($website2Id)->save(); + } + + /** + * Get query + * + * @param string $countryId + * @return string + */ + private function getQuery(string $countryId): string + { + return <<<QUERY +query { + country(id: {$countryId}) { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + } + + protected function tearDown(): void + { + $this->restoreConfig(); + parent::tearDown(); + } + + /** + * Set configuration + * + * @param string $path + * @param string $value + * @param string $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + string $path, + string $value, + string $scopeType, + ?string $scopeCode = null + ): void { + if ($this->configStorage->checkIsRecordExist($path, $scopeType, $scopeCode)) { + $this->origConfigs[] = [ + 'path' => $path, + 'value' => $this->configStorage->getValueFromDb($path, $scopeType, $scopeCode), + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } else { + $this->notExistingOrigConfigs[] = [ + 'path' => $path, + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } + $this->config->setValue($path, $value, $scopeType, $scopeCode); + } + + private function restoreConfig() + { + foreach ($this->origConfigs as $origConfig) { + $this->config->setValue( + $origConfig['path'], + $origConfig['value'], + $origConfig['scopeType'], + $origConfig['scopeCode'] + ); + } + $this->origConfigs = []; + + foreach ($this->notExistingOrigConfigs as $notExistingOrigConfig) { + $this->configStorage->deleteConfigFromDb( + $notExistingOrigConfig['path'], + $notExistingOrigConfig['scopeType'], + $notExistingOrigConfig['scopeCode'] + ); + } + $this->notExistingOrigConfigs = []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php index 55966fc0bce60..2552c96c5b482 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php @@ -10,71 +10,89 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the GraphQL endpoint's Coutries query + * Test the GraphQL endpoint's Countries query */ class CountryTest extends GraphQlAbstract { - public function testGetCountry() + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + */ + public function testGetDefaultStoreUSCountry() { - $query = <<<QUERY -query { - country(id: "US") { - id - two_letter_abbreviation - three_letter_abbreviation - full_name_locale - full_name_english - available_regions { - id - code - name - } + $result = $this->graphQlQuery($this->getQuery('US')); + $this->assertArrayHasKey('country', $result); + $this->assertEquals('US', $result['country']['id']); + $this->assertEquals('US', $result['country']['two_letter_abbreviation']); + $this->assertEquals('USA', $result['country']['three_letter_abbreviation']); + $this->assertEquals('United States', $result['country']['full_name_locale']); + $this->assertEquals('United States', $result['country']['full_name_english']); + $this->assertCount(65, $result['country']['available_regions']); + $this->assertArrayHasKey('id', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('code', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('name', $result['country']['available_regions'][0]); } -} -QUERY; - $result = $this->graphQlQuery($query); + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE + */ + public function testGetTestStoreDECountry() + { + $result = $this->graphQlQuery( + $this->getQuery('DE'), + [], + '', + ['Store' => 'test'] + ); $this->assertArrayHasKey('country', $result); - $this->assertArrayHasKey('id', $result['country']); - $this->assertArrayHasKey('two_letter_abbreviation', $result['country']); - $this->assertArrayHasKey('three_letter_abbreviation', $result['country']); - $this->assertArrayHasKey('full_name_locale', $result['country']); - $this->assertArrayHasKey('full_name_english', $result['country']); - $this->assertArrayHasKey('available_regions', $result['country']); + $this->assertEquals('DE', $result['country']['id']); + $this->assertEquals('DE', $result['country']['two_letter_abbreviation']); + $this->assertEquals('DEU', $result['country']['three_letter_abbreviation']); + $this->assertEquals('Germany', $result['country']['full_name_locale']); + $this->assertEquals('Germany', $result['country']['full_name_english']); + $this->assertCount(16, $result['country']['available_regions']); $this->assertArrayHasKey('id', $result['country']['available_regions'][0]); $this->assertArrayHasKey('code', $result['country']['available_regions'][0]); $this->assertArrayHasKey('name', $result['country']['available_regions'][0]); } /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/general/locale/code en_US + * @magentoConfigFixture default/general/country/allow US + * @magentoConfigFixture test_store general/locale/code en_US + * @magentoConfigFixture test_store general/country/allow US,DE */ - public function testGetCountryNotFoundException() + public function testGetDefaultStoreDECountryNotFoundException() { $this->expectException(\Exception::class); $this->expectExceptionMessage('GraphQL response contains errors: The country isn\'t available.'); - $query = <<<QUERY -query { - country(id: "BLAH") { - id - two_letter_abbreviation - three_letter_abbreviation - full_name_locale - full_name_english - available_regions { - id - code - name - } + $this->graphQlQuery($this->getQuery('DE')); } -} -QUERY; - $this->graphQlQuery($query); - } - - /** - */ public function testMissedInputParameterException() { $this->expectException(\Exception::class); @@ -94,4 +112,30 @@ public function testMissedInputParameterException() $this->graphQlQuery($query); } + + /** + * Get query + * + * @param string $countryId + * @return string + */ + private function getQuery(string $countryId): string + { + return <<<QUERY +query { + country(id: {$countryId}) { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataCacheTest.php new file mode 100644 index 0000000000000..f45c019686307 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataCacheTest.php @@ -0,0 +1,395 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Eav; + +use Magento\Eav\Model\AttributeRepository; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\Store\Model\StoreRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; + +/** + * Test caching for custom attribute metadata GraphQL query. + */ +class CustomAttributesMetadataCacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + parent::setUp(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheHitMiss() + { + $query = $this->getAttributeQuery("dropdown_attribute", "catalog_product"); + $response = $this->assertCacheMissAndReturnResponse($query, []); + $this->assertResponseFields( + $response['body']['customAttributeMetadata']['items'][0], + [ + 'attribute_code' => 'dropdown_attribute', + 'attribute_type' => 'String', + 'entity_type' => 'catalog_product', + 'input_type' => 'select', + ] + ); + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields( + $response['body']['customAttributeMetadata']['items'][0], + [ + 'attribute_code' => 'dropdown_attribute', + 'attribute_type' => 'String', + 'entity_type' => 'catalog_product', + 'input_type' => 'select', + ] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheDifferentStores() + { + $query = $this->getAttributeQuery("dropdown_attribute", "catalog_product"); + /** @var AttributeRepository $eavAttributeRepo */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + /** @var StoreRepository $storeRepo */ + $storeRepo = $this->objectManager->get(StoreRepository::class); + + $stores = $storeRepo->getList(); + $attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute"); + $options = $attribute->getOptions(); + + //prepare unique option values per-store + $storeOptions = []; + foreach ($options as $option) { + $optionValues = $option->getData(); + if (!empty($optionValues['value'])) { + $storeOptions['value'][$optionValues['value']] = []; + foreach ($stores as $store) { + $storeOptions['value'][$optionValues['value']][$store->getId()] = $store->getCode() + . '_' + . $optionValues['label']; + } + } + } + //save attribute with new option values + $attribute->addData(['option' => $storeOptions]); + $eavAttributeRepo->save($attribute); + + // get attribute metadata for test store and assert it missed the cache + $response = $this->assertCacheMissAndReturnResponse($query, ['Store' => 'test']); + $options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options']; + $this->assertOptionValuesPerStore($storeOptions, 'test', $stores, $options); + + // get attribute metadata for test store again and assert it has hit the cache + $response = $this->assertCacheHitAndReturnResponse($query, ['Store' => 'test']); + $options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options']; + $this->assertOptionValuesPerStore($storeOptions, 'test', $stores, $options); + + $response = $this->assertCacheMissAndReturnResponse($query, ['Store' => 'default']); + $options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options']; + $this->assertOptionValuesPerStore($storeOptions, 'default', $stores, $options); + + $response = $this->assertCacheHitAndReturnResponse($query, ['Store' => 'default']); + $options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options']; + $this->assertOptionValuesPerStore($storeOptions, 'default', $stores, $options); + } + + /** + * Assert attribute option labels for each store provided. + * + * @param array $storeOptions + * @param string $storeCode + * @param \Magento\Store\Api\Data\StoreInterface[] $stores + * @param array $options + * + * @return void + */ + private function assertOptionValuesPerStore($storeOptions, $storeCode, $stores, $options) + { + foreach ($options as $option) { + $this->assertEquals( + $storeOptions['value'][$option['value']][$stores[$storeCode]->getId()], + $option['label'] + ); + } + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheInvalidation() + { + $query = $this->getAttributeQuery("dropdown_attribute", "catalog_product"); + // check cache missed on first query + $response = $this->assertCacheMissAndReturnResponse($query, []); + $this->assertResponseFields( + $response['body']['customAttributeMetadata']['items'][0], + [ + 'attribute_code' => 'dropdown_attribute', + 'attribute_type' => 'String', + 'entity_type' => 'catalog_product', + 'input_type' => 'select', + ] + ); + // assert cache hit on second query + $this->assertCacheHitAndReturnResponse($query, []); + /** @var AttributeRepository $eavAttributeRepo */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute"); + $attribute->setIsRequired(1); + $eavAttributeRepo->save($attribute); + // assert cache miss after changes + $this->assertCacheMissAndReturnResponse($query, []); + // assert cache hits on second query after changes + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields( + $response['body']['customAttributeMetadata']['items'][0], + [ + 'attribute_code' => 'dropdown_attribute', + 'attribute_type' => 'String', + 'entity_type' => 'catalog_product', + 'input_type' => 'select', + ] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheInvalidationOnAttributeDelete() + { + $query = $this->getAttributeQuery("dropdown_attribute", "catalog_product"); + // check cache missed on first query + $response = $this->assertCacheMissAndReturnResponse($query, []); + $this->assertResponseFields( + $response['body']['customAttributeMetadata']['items'][0], + [ + 'attribute_code' => 'dropdown_attribute', + 'attribute_type' => 'String', + 'entity_type' => 'catalog_product', + 'input_type' => 'select', + ] + ); + // assert cache hit on second query + $this->assertCacheHitAndReturnResponse($query, []); + /** @var AttributeRepository $eavAttributeRepo */ + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute"); + $eavAttributeRepo->delete($attribute); + $this->assertQueryResultIsCacheMissWithError( + $query, + "GraphQL response contains errors: Internal server error" + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheMissingAttributeParam() + { + $query = $this->getAttributeQueryNoCode("catalog_product"); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: catalog_product." + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: catalog_product." + ); + + $query = $this->getAttributeQueryNoEntityType("dropdown_attribute"); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing entity_type for the input attribute_code: dropdown_attribute." + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing entity_type for the input attribute_code: dropdown_attribute." + ); + } + + /** + * Assert that query produces an error and the cache is missed. + * + * @param string $query + * @param string $expectedError + * @return void + * @throws \Exception + */ + private function assertQueryResultIsCacheMissWithError(string $query, string $expectedError) + { + $caughtException = null; + try { + // query for response, expect response to be present in exception + $this->graphQlQueryWithResponseHeaders($query, []); + } catch (ResponseContainsErrorsException $exception) { + $caughtException = $exception; + } + $this->assertInstanceOf( + ResponseContainsErrorsException::class, + $caughtException + ); + // cannot use expectException because need to assert the headers + $this->assertStringContainsString( + $expectedError, + $caughtException->getMessage() + ); + // assert that it's a miss + $this->assertEquals( + $caughtException->getResponseHeaders()['X-Magento-Cache-Debug'], + 'MISS' + ); + } + + /** + * Test cache invalidation when queried for attribute data of different entity types. + * Required for GraphQL FPC use-case since there is no attribute ID provided in the result. + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * + * @return void + */ + public function testCacheInvalidationMultiEntitySameCode() + { + $queryProduct = $this->getAttributeQuery("name", "catalog_product"); + $queryCategory = $this->getAttributeQuery("name", "catalog_category"); + // precache both product and category response + $this->assertCacheMissAndReturnResponse($queryProduct, []); + $this->assertCacheMissAndReturnResponse($queryCategory, []); + $eavAttributeRepo = $this->objectManager->get(AttributeRepository::class); + $attribute = $eavAttributeRepo->get("catalog_product", "name"); + $eavAttributeRepo->save($attribute); + // assert that product is invalidated for the same code but category is not touched + $this->assertCacheMissAndReturnResponse($queryProduct, []); + $this->assertCacheHitAndReturnResponse($queryCategory, []); + } + + /** + * Prepare and return GraphQL query for given entity type and code. + * + * @param string $code + * @param string $entityType + * @return string + */ + private function getAttributeQuery(string $code, string $entityType) : string + { + return <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + attribute_code:"{$code}", + entity_type:"{$entityType}" + } + ] + ) + { + items + { + attribute_code + attribute_type + entity_type + input_type + attribute_options{ + label + value + } + } + } + } +QUERY; + } + + /** + * Prepare and return GraphQL query for given entity type with no code. + * + * @param string $entityType + * + * @return string + */ + private function getAttributeQueryNoCode(string $entityType) : string + { + return <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + entity_type:"{$entityType}" + } + ] + ) + { + items + { + attribute_code + entity_type + } + } + } +QUERY; + } + + /** + * Prepare and return GraphQL query for given code with no entity type. + * + * @param string $code + * + * @return string + */ + private function getAttributeQueryNoEntityType(string $code) : string + { + return <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + attribute_code:"{$code}" + } + ] + ) + { + items + { + attribute_code + entity_type + } + } + } +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataV2CacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataV2CacheTest.php new file mode 100644 index 0000000000000..82f3f8e615c84 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataV2CacheTest.php @@ -0,0 +1,366 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Eav; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; +use Magento\PageCache\Model\Config; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; + +/** + * Test caching for custom attribute metadata GraphQL query. + */ +class CustomAttributesMetadataV2CacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var AttributeRepository + */ + private $attributeRepository; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepository::class); + parent::setUp(); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheHitMiss(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'code', 'expected_value' => $attribute->getAttributeCode()], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ] + public function testCacheMissAndHitDifferentStores(): void + { + /** @var StoreInterface $store2 */ + $store2 = DataFixtureStorageManager::getStorage()->get('store2'); + + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'code', 'expected_value' => $attribute->getAttributeCode()], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + // First query execution for a different store should result in a cache miss, while second one should be a hit + $response = $this->assertCacheMissAndReturnResponse($query, ['Store' => $store2->getCode()]); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, ['Store' => $store2->getCode()]); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute_1' + ), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean' + ], + 'attribute_2' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheInvalidation(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute_1'); + + /** @var AttributeInterface $attribute2 */ + $attribute2 = DataFixtureStorageManager::getStorage()->get('attribute_2'); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + // check cache missed on first query + $this->assertCacheMissAndReturnResponse($query, []); + // assert cache hit on second query + $this->assertCacheHitAndReturnResponse($query, []); + + $attribute->setIsRequired(true); + $this->attributeRepository->save($attribute); + // assert cache miss after changes + $this->assertCacheMissAndReturnResponse($query, []); + + $attribute2->setIsRequired(true); + $this->attributeRepository->save($attribute2); + + // assert cache hits on second query after changes, and cache is not invalidated when another entity changed + $this->assertCacheHitAndReturnResponse($query, []); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheInvalidationOnAttributeDelete() + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + $attributeCode = $attribute->getAttributeCode(); + + $query = $this->getAttributeQuery($attributeCode, "customer"); + + // check cache missed on first query + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'code', 'expected_value' => $attributeCode], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + // assert cache hit on second query + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + $this->attributeRepository->delete($attribute); + $assertionMap = [ + ['response_field' => 'type', 'expected_value' => 'ATTRIBUTE_NOT_FOUND'], + ['response_field' => 'message', 'expected_value' => sprintf( + 'Attribute code "%s" could not be found.', + $attributeCode + )] + ]; + $response = $this->assertCacheMissAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['errors'][0], $assertionMap); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheMissingAttributeParam(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $query = $this->getAttributeQueryNoCode("customer"); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: customer." + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: customer." + ); + + $query = $this->getAttributeQueryNoEntityType($attribute->getAttributeCode()); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + sprintf("Missing entity_type for the input attribute_code: %s.", $attribute->getAttributeCode()) + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + sprintf("Missing entity_type for the input attribute_code: %s.", $attribute->getAttributeCode()) + ); + } + + /** + * Assert that query produces an error and the cache is missed. + * + * @param string $query + * @param string $expectedError + * @return void + * @throws \Exception + */ + private function assertQueryResultIsCacheMissWithError(string $query, string $expectedError) + { + $caughtException = null; + try { + // query for response, expect response to be present in exception + $this->graphQlQueryWithResponseHeaders($query, []); + } catch (ResponseContainsErrorsException $exception) { + $caughtException = $exception; + } + $this->assertInstanceOf( + ResponseContainsErrorsException::class, + $caughtException + ); + // cannot use expectException because need to assert the headers + $this->assertStringContainsString( + $expectedError, + $caughtException->getMessage() + ); + // assert that it's a miss + $this->assertEquals( + 'MISS', + $caughtException->getResponseHeaders()['X-Magento-Cache-Debug'] + ); + } + + /** + * Prepare and return GraphQL query for given entity type and code. + * + * @param string $code + * @param string $entityType + * @return string + */ + private function getAttributeQuery(string $code, string $entityType) : string + { + return <<<QUERY +{ + customAttributeMetadataV2(attributes: [{attribute_code:"{$code}", entity_type:"{$entityType}"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + options { + label + value + } + } + errors { + type + message + } + } +} +QUERY; + } + + /** + * Prepare and return GraphQL query for given entity type with no code. + * + * @param string $entityType + * + * @return string + */ + private function getAttributeQueryNoCode(string $entityType) : string + { + return <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + entity_type:"{$entityType}" + } + ] + ) + { + items + { + attribute_code + entity_type + } + } + } +QUERY; + } + + /** + * Prepare and return GraphQL query for given code with no entity type. + * + * @param string $code + * + * @return string + */ + private function getAttributeQueryNoEntityType(string $code) : string + { + return <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + attribute_code:"{$code}" + } + ] + ) + { + items + { + attribute_code + entity_type + } + } + } +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListCacheTest.php new file mode 100644 index 0000000000000..576c7472e1400 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListCacheTest.php @@ -0,0 +1,425 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\EavGraphQl; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Test\Fixture\CustomerAttribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\AttributeFactory; +use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\PageCache\Model\Config; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; + +/** + * Test caching for attributes list GraphQL query. + */ +class AttributesListCacheTest extends GraphQLPageCacheAbstract +{ + private const QUERY = <<<QRY + { + attributesList(entityType: CUSTOMER) { + items { + code + } + errors { + type + message + } + } + } +QRY; + + private const QUERY_ADDRESS = <<<QRY + { + attributesList(entityType: CUSTOMER_ADDRESS) { + items { + code + } + errors { + type + message + } + } + } +QRY; + + /** + * @var AttributeRepository + */ + private $eavAttributeRepo; + + /** + * @var AttributeFactory + */ + private $attributeFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->eavAttributeRepo = Bootstrap::getObjectManager()->get(AttributeRepository::class); + /** @var AttributeFactory $attributeFactory */ + $this->attributeFactory = Bootstrap::getObjectManager()->create(AttributeFactory::class); + parent::setUp(); + } + + /** + * Obtains cache ID header from response + * + * @param string $query + * @return string + */ + private function getCacheIdHeader(string $query, array $headers = []): string + { + $response = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + $headers + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + return $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ), + ] + public function testAttributesListCacheMissAndHit() + { + /** @var AttributeInterface $attribute0 */ + $attribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + $cacheId = $this->getCacheIdHeader(self::QUERY); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $attribute = end($response['body']['attributesList']['items']); + $this->assertEquals($attribute0->getAttributeCode(), $attribute['code']); + + // Modify an attribute present in the response of the previous query to check cache invalidation + $attribute0->setDefaultValue('default_value'); + $this->eavAttributeRepo->save($attribute0); + + // First query execution should result in a cache miss, while second one should be a cache hit + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ), + ] + public function testAttributesListCacheMissAndHitDifferentStores() + { + /** @var StoreInterface $store2 */ + $store2 = DataFixtureStorageManager::getStorage()->get('store2'); + $cacheIdStore1 = $this->getCacheIdHeader(self::QUERY); + $cacheIdStore2 = $this->getCacheIdHeader(self::QUERY, ['Store' => $store2->getCode()]); + + /** @var AttributeInterface $attribute0 */ + $attribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore1] + ); + $attribute = end($response['body']['attributesList']['items']); + $this->assertEquals($attribute0->getAttributeCode(), $attribute['code']); + + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore1] + ); + + // First query execution for a different store should result in a cache miss, while second one should be a hit + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY, + [ + 'Store' => $store2->getCode(), + CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore2 + ] + ); + $attribute = end($response['body']['attributesList']['items']); + $this->assertEquals($attribute0->getAttributeCode(), $attribute['code']); + + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [ + 'Store' => $store2->getCode(), + CacheIdCalculator::CACHE_ID_HEADER => $cacheIdStore2 + ] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ) + ] + public function testAttributeListCacheInvalidateOnAttributeDelete() + { + /** @var AttributeInterface $customerAttribute0 */ + $customerAttribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + $cacheId = $this->getCacheIdHeader(self::QUERY); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $deletedAttributeCode = $customerAttribute0->getAttributeCode(); + $this->eavAttributeRepo->delete($customerAttribute0); + + $response = $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + foreach ($response['body']['attributesList']['items'] as $item) { + if (in_array($deletedAttributeCode, $item)) { + $this->fail(sprintf( + "Deleted attribute '%s' found in query response", + $deletedAttributeCode + )); + } + } + + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'default_value' => 'initial value', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ) + ] + public function testAttributeListCacheInvalidateOnAttributeEdit() + { + /** @var AttributeInterface $customerAttribute0 */ + $customerAttribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + $cacheId = $this->getCacheIdHeader(self::QUERY); + $cacheAddressId = $this->getCacheIdHeader(self::QUERY_ADDRESS); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheAddressId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $customerAttribute0->setDefaultValue('after change default value'); + $this->eavAttributeRepo->save($customerAttribute0); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $this->assertCacheHitAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheAddressId] + ); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_address_attribute_0' + ), + ] + public function testAttributeListChangeOnlyAffectsResponsesWithEntity() + { + /** @var AttributeInterface $customerAttribute0 */ + $customerAttribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + + /** @var AttributeInterface $customerAttribute0 */ + $customerAddressAttribute0 = DataFixtureStorageManager::getStorage()->get('customer_address_attribute_0'); + $cacheId = $this->getCacheIdHeader(self::QUERY); + $cacheAddressId = $this->getCacheIdHeader(self::QUERY_ADDRESS); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $attribute = end($response['body']['attributesList']['items']); + $this->assertEquals($customerAttribute0->getAttributeCode(), $attribute['code']); + + $this->assertCacheMissAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheAddressId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheAddressId] + ); + + $customerAttribute0->setAttributeCode($customerAttribute0->getAttributeCode() . '_modified'); + $this->eavAttributeRepo->save($customerAttribute0); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $response = $this->assertCacheHitAndReturnResponse( + self::QUERY_ADDRESS, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheAddressId] + ); + + $attribute = end($response['body']['attributesList']['items']); + $this->assertEquals($customerAddressAttribute0->getAttributeCode(), $attribute['code']); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + ] + public function testAttributesListCacheMissAndHitNewAttribute() + { + $cacheId = $this->getCacheIdHeader(self::QUERY); + + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $newAttributeCreate = Bootstrap::getObjectManager()->get(CustomerAttribute::class); + /** @var AttributeInterface $newAttribute */ + $newAttribute = $newAttributeCreate->apply([ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ]); + + // First query execution should result in a cache miss, while second one should be a cache hit + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertCacheHitAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + $this->eavAttributeRepo->delete($newAttribute); + + // Check that the same mentioned above applies if we delete an attribute present in the response + $this->assertCacheMissAndReturnResponse( + self::QUERY, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListTest.php new file mode 100644 index 0000000000000..54d7cc8c77b80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/EavGraphQl/AttributesListTest.php @@ -0,0 +1,339 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\EavGraphQl; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Eav\Test\Fixture\Attribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Sales\Setup\SalesSetup; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Catalog\Test\Fixture\Attribute as ProductAttribute; +use Magento\Customer\Test\Fixture\CustomerAttribute; + +/** + * Test EAV attributes metadata retrieval for entity type via GraphQL API + */ +#[ + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_0' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_1' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_2' + ), + DataFixture( + CustomerAttribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'customer_attribute_3' + ), + DataFixture( + ProductAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean', + 'is_visible_on_front' => 1, + ], + 'catalog_attribute_3' + ), + DataFixture( + ProductAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean', + 'is_visible_on_front' => 1, + 'is_comparable' => 1 + ], + 'catalog_attribute_4' + ), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => SalesSetup::CREDITMEMO_PRODUCT_ENTITY_TYPE_ID, + 'frontend_input' => 'boolean', + 'source_model' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean' + ], + 'credit_memo_attribute_5' + ) +] +class AttributesListTest extends GraphQlAbstract +{ + private const ATTRIBUTE_NOT_FOUND_ERROR = "Attribute was not found in query result"; + + /** + * @var AttributeInterface|null + */ + private $creditmemoAttribute5; + + /** + * @var AttributeInterface|null + */ + private $customerAttribute0; + + /** + * @var AttributeInterface|null + */ + private $customerAttribute1; + + /** + * @var AttributeInterface|null + */ + private $customerAttribute2; + + /** + * @var AttributeInterface|null + */ + private $customerAttribute3; + + /** + * @var AttributeInterface|null + */ + private $catalogAttribute3; + + /** + * @var AttributeInterface|null + */ + private $catalogAttribute4; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->creditmemoAttribute5 = DataFixtureStorageManager::getStorage()->get('credit_memo_attribute_5'); + $this->customerAttribute0 = DataFixtureStorageManager::getStorage()->get('customer_attribute_0'); + $this->customerAttribute1 = DataFixtureStorageManager::getStorage()->get('customer_attribute_1'); + $this->customerAttribute2 = DataFixtureStorageManager::getStorage()->get('customer_attribute_2'); + $this->customerAttribute3 = DataFixtureStorageManager::getStorage()->get('customer_attribute_3'); + $this->customerAttribute3->setIsVisible(false)->save(); + $this->catalogAttribute3 = DataFixtureStorageManager::getStorage()->get('catalog_attribute_3'); + $this->catalogAttribute4 = DataFixtureStorageManager::getStorage()->get('catalog_attribute_4'); + } + + public function testAttributesListForCustomerEntityType(): void + { + $queryResult = $this->graphQlQuery(<<<QRY + { + attributesList(entityType: CUSTOMER) { + items { + code + } + errors { + type + message + } + } + } +QRY); + $this->assertCustomerResults($queryResult); + $this->assertEmpty(count($queryResult['attributesList']['errors'])); + } + + public function testAttributesListForCatalogProductEntityType(): void + { + $queryResult = $this->graphQlQuery(<<<QRY + { + attributesList(entityType: CATALOG_PRODUCT) { + items { + code + } + errors { + type + message + } + } + } +QRY); + $this->assertArrayHasKey('items', $queryResult['attributesList'], 'Query result does not contain items'); + $this->assertGreaterThanOrEqual(2, count($queryResult['attributesList']['items'])); + + $this->assertEquals( + $this->catalogAttribute3->getAttributeCode(), + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->catalogAttribute3->getAttributeCode() + )['code'], + self::ATTRIBUTE_NOT_FOUND_ERROR + ); + $this->assertEquals( + $this->catalogAttribute4->getAttributeCode(), + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->catalogAttribute4->getAttributeCode() + )['code'], + self::ATTRIBUTE_NOT_FOUND_ERROR + ); + $this->assertEquals( + [], + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->creditmemoAttribute5->getAttributeCode() + ) + ); + } + + public function testAttributesListFilterForCatalogProductEntityType(): void + { + $queryResult = $this->graphQlQuery(<<<QRY + { + attributesList(entityType: CATALOG_PRODUCT, filters: {is_visible_on_front: true, is_comparable: true}) { + items { + code + ... on CatalogAttributeMetadata { + is_comparable + is_visible_on_front + } + } + errors { + type + message + } + } + } +QRY); + $this->assertArrayHasKey('items', $queryResult['attributesList'], 'Query result does not contain items'); + $this->assertEquals( + [ + 'attributesList' => [ + 'items' => [ + 0 => [ + 'code' => $this->catalogAttribute4->getAttributeCode(), + 'is_comparable' => true, + 'is_visible_on_front' => true + ] + ], + 'errors' => [] + ] + ], + $queryResult + ); + } + + public function testAttributesListAnyFilterApply(): void + { + $queryResult = $this->graphQlQuery(<<<QRY + { + attributesList(entityType: CUSTOMER, filters: {is_filterable: true}) { + items { + code + ... on CatalogAttributeMetadata { + is_filterable + } + } + errors { + type + message + } + } + } +QRY); + $this->assertCustomerResults($queryResult); + $this->assertEquals(1, count($queryResult['attributesList']['errors'])); + $this->assertEquals('FILTER_NOT_FOUND', $queryResult['attributesList']['errors'][0]['type']); + $this->assertEquals( + 'Cannot filter by "is_filterable" as that field does not belong to "customer".', + $queryResult['attributesList']['errors'][0]['message'] + ); + } + + /** + * Finds attribute in query result + * + * @param array $items + * @param string $attribute_code + * @return array + */ + private function getAttributeByCode(array $items, string $attribute_code): array + { + $attribute = array_filter($items, function ($item) use ($attribute_code) { + return $item['code'] == $attribute_code; + }); + return $attribute[array_key_first($attribute)] ?? []; + } + + /** + * @param array $queryResult + */ + private function assertCustomerResults(array $queryResult): void + { + $this->assertArrayHasKey('items', $queryResult['attributesList'], 'Query result does not contain items'); + $this->assertGreaterThanOrEqual(3, count($queryResult['attributesList']['items'])); + + $this->assertEquals( + $this->customerAttribute0->getAttributeCode(), + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->customerAttribute0->getAttributeCode() + )['code'], + self::ATTRIBUTE_NOT_FOUND_ERROR + ); + + $this->assertEquals( + $this->customerAttribute1->getAttributeCode(), + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->customerAttribute1->getAttributeCode() + )['code'], + self::ATTRIBUTE_NOT_FOUND_ERROR + ); + $this->assertEquals( + $this->customerAttribute2->getAttributeCode(), + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->customerAttribute2->getAttributeCode() + )['code'], + self::ATTRIBUTE_NOT_FOUND_ERROR + ); + $this->assertEquals( + [], + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->customerAttribute3->getAttributeCode() + ) + ); + $this->assertEquals( + [], + $this->getAttributeByCode( + $queryResult['attributesList']['items'], + $this->creditmemoAttribute5->getAttributeCode() + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/ErrorHandlerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/ErrorHandlerTest.php index 353a9e34125bb..d1130f22599e1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/ErrorHandlerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/ErrorHandlerTest.php @@ -43,7 +43,7 @@ public function testErrorHandlerReportsFirstErrorOnly() self::assertCount(1, $responseData['errors']); $errorMsg = $responseData['errors'][0]['message']; - self::assertMatchesRegularExpression('/Unknown directive \"aaaaaa\"./', $errorMsg); + self::assertMatchesRegularExpression('/Unknown directive \"@aaaaaa\"./', $errorMsg); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GraphQlTypeValidationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GraphQlTypeValidationTest.php index 01597f23b49e4..f5e25ab6ea2bb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GraphQlTypeValidationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GraphQlTypeValidationTest.php @@ -68,7 +68,7 @@ public function testIntegerExpectedWhenFloatProvided() 'currentPage' => 1.1 ]; $this->expectException(\Exception::class); - $this->expectExceptionMessage('Variable "$currentPage" got invalid value 1.1; Expected type Int; ' . + $this->expectExceptionMessage('Variable "$currentPage" got invalid value 1.1; ' . 'Int cannot represent non-integer value: 1.1'); $this->graphQlQuery($query, $variables); } @@ -192,7 +192,7 @@ public function testStringExpectedWhenArrayProvided() 'quantity' => '5.60' ]; $this->expectException(\Exception::class); - $this->expectExceptionMessage('Variable "$sku" got invalid value ["123.78"]; Expected type String; ' . + $this->expectExceptionMessage('Variable "$sku" got invalid value ["123.78"]; ' . 'String cannot represent a non string value: ["123.78"]'); $this->graphQlMutation($query, $variables); } @@ -215,8 +215,8 @@ public function testFloatExpectedWhenNonNumericStringProvided() 'quantity' => 'ten' ]; $this->expectException(\Exception::class); - $this->expectExceptionMessage('Variable "$quantity" got invalid value "ten"; Expected type Float; ' . - 'Float cannot represent non numeric value: ten'); + $this->expectExceptionMessage('Variable "$quantity" got invalid value "ten"; ' . + 'Float cannot represent non numeric value: "ten"'); $this->graphQlMutation($query, $variables); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php index ec0e49cc55153..17c18521f2a2c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php @@ -8,11 +8,14 @@ namespace Magento\GraphQl\Newsletter\Customer; use Exception; +use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\CustomerAuthUpdate; use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\AuthenticationException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -40,6 +43,10 @@ class SubscribeEmailToNewsletterTest extends GraphQlAbstract * @var SubscriberResourceModel */ private $subscriberResource; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; /** * @inheritDoc @@ -47,6 +54,7 @@ class SubscribeEmailToNewsletterTest extends GraphQlAbstract protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); @@ -146,10 +154,17 @@ public function testNewsletterSubscriptionWithAnotherCustomerEmail() { $query = $this->getQuery('customer2@search.example.com'); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Cannot create a newsletter subscription.' . "\n"); - - $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + $guestLoginConfig = $this->scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); + + if ($guestLoginConfig) { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cannot create a newsletter subscription.' . "\n"); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php index f0a933609c762..52b8ac2e3f046 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php @@ -31,6 +31,9 @@ protected function setUp(): void $this->subscriberResource = $objectManager->get(SubscriberResourceModel::class); } + /** + * @magentoConfigFixture default_store newsletter/subscription/allow_guest_subscribe 1 + */ public function testAddEmailIntoNewsletterSubscription() { $query = $this->getQuery('guest@example.com'); @@ -41,6 +44,9 @@ public function testAddEmailIntoNewsletterSubscription() self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); } + /** + * @magentoConfigFixture default_store newsletter/subscription/allow_guest_subscribe 1 + */ public function testNewsletterSubscriptionWithIncorrectEmailFormat() { $query = $this->getQuery('guest.example.com'); @@ -68,6 +74,7 @@ public function testNewsletterSubscriptionWithDisallowedGuestSubscription() /** * @magentoApiDataFixture Magento/Newsletter/_files/guest_subscriber.php + * @magentoConfigFixture default_store newsletter/subscription/allow_guest_subscribe 1 */ public function testNewsletterSubscriptionWithAlreadySubscribedEmail() { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php index 6fb587fae7365..93e6caebe2ba2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php @@ -9,30 +9,21 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the caching works properly for products and categories + * Test the cache works properly for products and categories */ -class CacheTagTest extends GraphQlAbstract +class CacheTagTest extends GraphQLPageCacheAbstract { /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Test if Magento cache tags and debug headers for products are generated properly + * Test cache invalidation for products * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php */ - public function testCacheTagsAndCacheDebugHeaderForProducts() + public function testCacheInvalidationForProducts() { $productSku='simple2'; $query @@ -48,16 +39,15 @@ public function testCacheTagsAndCacheDebugHeaderForProducts() } } QUERY; - - // Cache-debug should be a MISS when product is queried for first time - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - // Cache-debug should be a HIT for the second round - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + // Cache should be a MISS when product is queried for first time + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + // Obtain the X-Magento-Cache-Id from the response + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); @@ -65,87 +55,82 @@ public function testCacheTagsAndCacheDebugHeaderForProducts() $product = $productRepository->get($productSku, false, null, true); $product->setPrice(15); $productRepository->save($product); - // Cache invalidation happens and cache-debug header value is a MISS after product update - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $expectedCacheTags = ['cat_p','cat_p_' . $product->getId(),'FPC']; - $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - foreach ($expectedCacheTags as $expectedCacheTag) { - $this->assertContains($expectedCacheTag, $actualCacheTags); - } + + // Cache invalidation happens and cache header value is a MISS after product update + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); } /** - * Test if X-Magento-Tags for categories are generated properly - * - * Also tests the use case for cache invalidation + * Test cache is invalidated properly for categories * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php */ - public function testCacheTagForCategoriesWithProduct() + public function testCacheInvalidationForCategoriesWithProduct() { $firstProductSku = 'simple333'; $secondProductSku = 'simple444'; - $categoryId ='4'; /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); /** @var Product $firstProduct */ $firstProduct = $productRepository->get($firstProductSku, false, null, true); - /** @var Product $secondProduct */ - $secondProduct = $productRepository->get($secondProductSku, false, null, true); - - $categoryQueryVariables =[ - 'id' => $categoryId, - 'pageSize'=> 10, - 'currentPage' => 1 - ]; $product1Query = $this->getProductQuery($firstProductSku); $product2Query =$this->getProductQuery($secondProductSku); $categoryQuery = $this->getCategoryQuery(); // cache-debug header value should be a MISS when category is loaded first time - $responseMiss = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedCacheTags = - [ - 'cat_c', - 'cat_c_' . $categoryId, - 'cat_p', - 'cat_p_' . $firstProduct->getId(), - 'cat_p_' . $secondProduct->getId(), - 'FPC' - ]; - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $responseMissOnCategoryQuery = $this->graphQlQueryWithResponseHeaders($categoryQuery); + $cacheIdOfCategoryQuery = $responseMissOnCategoryQuery['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $categoryQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfCategoryQuery] + ); // Cache-debug header should be a MISS for product 1 on first request $responseFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); - $this->assertEquals('MISS', $responseFirstProduct['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseFirstProduct['headers']); + $cacheIdOfFirstProduct = $responseFirstProduct['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS on the first product + $this->assertCacheMissAndReturnResponse( + $product1Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFirstProduct] + ); + // Cache-debug header should be a MISS for product 2 during first load - $responseSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); - $this->assertEquals('MISS', $responseSecondProduct['headers']['X-Magento-Cache-Debug']); + $responseMissSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); + $cacheIdOfSecondProduct = $responseMissSecondProduct['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time for product 2 + $this->assertCacheMissAndReturnResponse( + $product2Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfSecondProduct] + ); + // updating product1 $firstProduct->setPrice(20); $productRepository->save($firstProduct); - // cache-debug header value should be MISS after updating product1 and reloading the Category - $responseMissCategory = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissCategory['headers']); - $this->assertEquals('MISS', $responseMissCategory['headers']['X-Magento-Cache-Debug']); + + // Verify we obtain a cache MISS after the first product update and category reloading + $this->assertCacheMissAndReturnResponse( + $categoryQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfCategoryQuery] + ); // cache-debug should be a MISS for product 1 after it is updated - cache invalidation - $responseMissFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissFirstProduct['headers']); - $this->assertEquals('MISS', $responseMissFirstProduct['headers']['X-Magento-Cache-Debug']); - // Cache-debug header should be a HIT for product 2 - $responseHitSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHitSecondProduct['headers']); - $this->assertEquals('HIT', $responseHitSecondProduct['headers']['X-Magento-Cache-Debug']); + // Verify we obtain a cache MISS after the first product update + $this->assertCacheMissAndReturnResponse( + $product1Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFirstProduct] + ); + + // Cache-debug header responses for product 2 and should be a HIT for product 2 + // Verify we obtain a cache HIT on the second product after product 1 update + $this->assertCacheHitAndReturnResponse( + $product2Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfSecondProduct] + ); } /** @@ -179,13 +164,13 @@ private function getProductQuery(string $productSku): string private function getCategoryQuery(): string { $categoryQueryString = <<<QUERY -query GetCategoryQuery(\$id: Int!, \$pageSize: Int!, \$currentPage: Int!) { - category(id: \$id) { +query { + category(id: 4) { id description name product_count - products(pageSize: \$pageSize, currentPage: \$currentPage) { + products(pageSize: 10, currentPage: 1) { items { id name @@ -196,7 +181,6 @@ private function getCategoryQuery(): string } } QUERY; - return $categoryQueryString; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php index 0400919484f81..bfc38ae5fac7c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php @@ -9,49 +9,19 @@ use Magento\Cms\Model\Block; use Magento\Cms\Model\BlockRepository; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the caching works properly for CMS Blocks + * Test the cache works properly for CMS Blocks */ -class BlockCacheTest extends GraphQlAbstract +class BlockCacheTest extends GraphQLPageCacheAbstract { - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Test that X-Magento-Tags are correct - * - * @magentoApiDataFixture Magento/Cms/_files/block.php - */ - public function testCacheTagsHaveExpectedValue() - { - $blockIdentifier = 'fixture_block'; - $blockRepository = Bootstrap::getObjectManager()->get(BlockRepository::class); - $block = $blockRepository->getById($blockIdentifier); - $blockId = $block->getId(); - $query = $this->getBlockQuery([$blockIdentifier]); - - //cache-debug should be a MISS on first request - $response = $this->graphQlQueryWithResponseHeaders($query); - - $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); - $actualTags = explode(',', $response['headers']['X-Magento-Tags']); - $expectedTags = ["cms_b", "cms_b_{$blockId}", "cms_b_{$blockIdentifier}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - } - /** * Test the second request for the same block will return a cached result * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/block.php */ public function testCacheIsUsedOnSecondRequest() @@ -59,15 +29,18 @@ public function testCacheIsUsedOnSecondRequest() $blockIdentifier = 'fixture_block'; $query = $this->getBlockQuery([$blockIdentifier]); - //cache-debug should be a MISS on first request - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a MISS on first request and HIT on the second request + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $responseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); //cached data should be correct $this->assertNotEmpty($responseHit['body']); $this->assertArrayNotHasKey('errors', $responseHit['body']); @@ -79,6 +52,7 @@ public function testCacheIsUsedOnSecondRequest() /** * Test that cache is invalidated when block is updated * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/blocks.php * @magentoApiDataFixture Magento/Cms/_files/block.php */ @@ -89,30 +63,63 @@ public function testCacheIsInvalidatedOnBlockUpdate() $fixtureBlockQuery = $this->getBlockQuery([$fixtureBlockIdentifier]); $enabledBlockQuery = $this->getBlockQuery([$enabledBlockIdentifier]); - //cache-debug should be a MISS on first request - $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); - $enabledBlockMiss = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('MISS', $enabledBlockMiss['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a MISS on first request and HIT on second request + $fixtureBlock = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $fixtureBlock['headers']); + $cacheIdOfFixtureBlock = $fixtureBlock['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); - //cache-debug should be a HIT on second request - $fixtureBlockHit = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('HIT', $fixtureBlockHit['headers']['X-Magento-Cache-Debug']); - $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); + $enabledBlock = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $enabledBlock['headers']); + $cacheIdOfEnabledBlock = $enabledBlock['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + + //cache should be a HIT on second request + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + //updating content on fixture block $newBlockContent = 'New block content!!!'; $this->updateBlockContent($fixtureBlockIdentifier, $newBlockContent); - //cache-debug should be a MISS after update the block - $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); - $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); - //updated block data should be correct - $this->assertNotEmpty($fixtureBlockMiss['body']); - $blocks = $fixtureBlockMiss['body']['cmsBlocks']['items']; - $this->assertArrayNotHasKey('errors', $fixtureBlockMiss['body']); + // Verify we obtain a cache MISS on the fixture block query + // after the content update on the fixture block + $this->assertCacheMissAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + + $fixtureBlockHitResponse = $this->assertCacheHitAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + + //Verify we obtain a cache HIT on the enabled block query after the fixture block is updated + $this->assertCacheHitAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + + //updated block data should be correct on fixture block + $this->assertNotEmpty($fixtureBlockHitResponse['body']); + $blocks = $fixtureBlockHitResponse['body']['cmsBlocks']['items']; + $this->assertArrayNotHasKey('errors', $fixtureBlockHitResponse['body']); $this->assertEquals($fixtureBlockIdentifier, $blocks[0]['identifier']); $this->assertEquals('CMS Block Title', $blocks[0]['title']); $this->assertEquals($newBlockContent, $blocks[0]['content']); @@ -131,7 +138,6 @@ private function updateBlockContent($identifier, $newContent): Block $block = $blockRepository->getById($identifier); $block->setContent($newContent); $blockRepository->save($block); - return $block; } @@ -145,7 +151,7 @@ private function getBlockQuery(array $identifiers): string { $identifiersString = implode(',', $identifiers); $query = <<<QUERY - { + { cmsBlocks(identifiers: ["$identifiersString"]) { items { title diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php index 355c23769af09..3eea42d4f5b15 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php @@ -9,13 +9,14 @@ use Magento\Cms\Model\GetPageByIdentifier; use Magento\Cms\Model\PageRepository; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the caching works properly for CMS Pages + * Test the cache works properly for CMS Pages */ -class PageCacheTest extends GraphQlAbstract +class PageCacheTest extends GraphQLPageCacheAbstract { /** * @var GetPageByIdentifier @@ -27,37 +28,13 @@ class PageCacheTest extends GraphQlAbstract */ protected function setUp(): void { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); $this->pageByIdentifier = Bootstrap::getObjectManager()->get(GetPageByIdentifier::class); } - /** - * Test that X-Magento-Tags are correct - * - * @magentoApiDataFixture Magento/Cms/_files/pages.php - */ - public function testCacheTagsHaveExpectedValue() - { - $pageIdentifier = 'page100'; - $page = $this->pageByIdentifier->execute($pageIdentifier, 0); - $pageId = (int) $page->getId(); - - $query = $this->getPageQuery($pageId); - - //cache-debug should be a MISS on first request - $response = $this->graphQlQueryWithResponseHeaders($query); - - $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); - $actualTags = explode(',', $response['headers']['X-Magento-Tags']); - $expectedTags = ["cms_p", "cms_p_{$pageId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - } - /** * Test the second request for the same page will return a cached result * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testCacheIsUsedOnSecondRequest() @@ -68,15 +45,18 @@ public function testCacheIsUsedOnSecondRequest() $query = $this->getPageQuery($pageId); - //cache-debug should be a MISS on first request - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + // Obtain the X-Magento-Cache-Id from the response + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $responseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + //cached data should be correct $this->assertNotEmpty($responseHit['body']); $this->assertArrayNotHasKey('errors', $responseHit['body']); @@ -87,6 +67,7 @@ public function testCacheIsUsedOnSecondRequest() /** * Test that cache is invalidated when page is updated * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testCacheIsInvalidatedOnPageUpdate() @@ -102,31 +83,61 @@ public function testCacheIsInvalidatedOnPageUpdate() $pageBlankQuery = $this->getPageQuery($pageBlankId); //cache-debug should be a MISS on first request - $page100Miss = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('MISS', $page100Miss['headers']['X-Magento-Cache-Debug']); - $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); + $page100Response = $this->graphQlQueryWithResponseHeaders($page100Query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $page100Response['headers']); + $cacheIdPage100Response = $page100Response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + + $pageBlankResponse = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $pageBlankResponse['headers']); + $cacheIdPageBlankResponse = $pageBlankResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); - //cache-debug should be a HIT on second request - $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); - $pageBlankHit = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('HIT', $pageBlankHit['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a HIT on second request for page100 + $this->assertCacheHitAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + //cache-debug should be a HIT on second request for page blank + $this->assertCacheHitAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + //updating the blank page $pageRepository = Bootstrap::getObjectManager()->get(PageRepository::class); $newPageContent = 'New page content for blank page.'; $pageBlank->setContent($newPageContent); $pageRepository->save($pageBlank); - //cache-debug should be a MISS after updating the page - $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); - $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); - //updated page data should be correct - $this->assertNotEmpty($pageBlankMiss['body']); - $pageData = $pageBlankMiss['body']['cmsPage']; - $this->assertArrayNotHasKey('errors', $pageBlankMiss['body']); + // Verify we obtain a cache MISS on page blank query after updating the page blank + $this->assertCacheMissAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + $pageBlankResponseHitAfterUpdate = $this->assertCacheHitAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + + // Verify we obtain a cache HIT on page 100 query after updating the page blank + $this->assertCacheHitAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + + //updated page data should be correct for blank page + $this->assertNotEmpty($pageBlankResponseHitAfterUpdate['body']); + $pageData = $pageBlankResponseHitAfterUpdate['body']['cmsPage']; + $this->assertArrayNotHasKey('errors', $pageBlankResponseHitAfterUpdate['body']); $this->assertEquals('Cms Page Design Blank', $pageData['title']); $this->assertEquals($newPageContent, $pageData['content']); } @@ -140,8 +151,8 @@ public function testCacheIsInvalidatedOnPageUpdate() private function getPageQuery(int $pageId): string { $query = <<<QUERY -{ - cmsPage(id: $pageId) { +{ + cmsPage(id: $pageId) { title url_key content diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php index 40280f5c7b2c7..794eabfe299dd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php @@ -230,7 +230,7 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() $this->assertEquals( 'EUR', $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], - 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is unexpected' + 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is expected' ); // test cached store + currency header in Euros diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php index ee5e186ee56c9..13382a075ebdc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php @@ -7,41 +7,40 @@ namespace Magento\GraphQl\PageCache\Quote\Guest; -use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; /** * Test cart queries are not cached * * @magentoApiDataFixture Magento/Catalog/_files/products.php */ -class CartCacheTest extends GraphQlAbstract +class CartCacheTest extends GraphQLPageCacheAbstract { /** * @inheritdoc + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - public function testCartIsNotCached() { - $qty = 2; + $quantity = 2; $sku = 'simple'; $cartId = $this->createEmptyCart(); - $this->addSimpleProductToCart($cartId, $qty, $sku); + $this->addSimpleProductToCart($cartId, $quantity, $sku); $getCartQuery = $this->getCartQuery($cartId); $responseMiss = $this->graphQlQueryWithResponseHeaders($getCartQuery); $this->assertArrayHasKey('cart', $responseMiss['body']); $this->assertArrayHasKey('items', $responseMiss['body']['cart']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseMiss['headers']); + $cacheId = $responseMiss['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($getCartQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); - /** Cache debug header value is still a MISS for any subsequent request */ - $responseMissNext = $this->graphQlQueryWithResponseHeaders($getCartQuery); - $this->assertEquals('MISS', $responseMissNext['headers']['X-Magento-Cache-Debug']); + // Cache debug header value is still a MISS for any subsequent request + // Verify we obtain a cache MISS the second time + $this->assertCacheMissAndReturnResponse($getCartQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); } /** @@ -68,21 +67,21 @@ private function createEmptyCart(): string * Add simple product to the cart using the maskedQuoteId * * @param string $maskedCartId - * @param int $qty + * @param float $quantity * @param string $sku */ - private function addSimpleProductToCart(string $maskedCartId, int $qty, string $sku): void + private function addSimpleProductToCart(string $maskedCartId, float $quantity, string $sku): void { $addProductToCartQuery = <<<QUERY - mutation { + mutation { addSimpleProductsToCart( input: { cart_id: "{$maskedCartId}" cart_items: [ { data: { - qty: $qty + quantity: $quantity sku: "$sku" } } @@ -91,7 +90,7 @@ private function addSimpleProductToCart(string $maskedCartId, int $qty, string $ ) { cart { items { - qty + quantity product { sku } @@ -117,7 +116,7 @@ private function getCartQuery(string $maskedQuoteId): string cart(cart_id: "{$maskedQuoteId}") { items { id - qty + quantity product { sku } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php index 226ca283c9dcd..0e453e13455f3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php @@ -9,107 +9,82 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\UrlRewrite\Model\UrlFinderInterface; /** - * Test caching works for url resolver. + * Test cache works properly for url resolver. */ -class UrlResolverCacheTest extends GraphQlAbstract +class UrlResolverCacheTest extends GraphQLPageCacheAbstract { /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Tests that X-Magento-tags and cache debug headers are correct for product urlResolver + * Tests cache works properly for product urlResolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCacheTagsForProducts() + public function testUrlResolverCachingForProducts() { - $productSku = 'p002'; $urlKey = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var Product $product */ - $product = $productRepository->get($productSku, false, null, true); $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cat_p", "cat_p_{$product->getId()}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + // Obtain the X-Magento-Cache-Id from the response + $response = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForProducts = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForProducts] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForProducts] + ); + //cached data should be correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('PRODUCT', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('PRODUCT', $cachedResponse['body']['urlResolver']['type']); } + /** - * Tests that X-Magento-tags and cache debug headers are correct for category urlResolver + * Tests cache invalidation for category urlResolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCacheTagsForCategory() + public function testUrlResolverCachingForCategory() { $categoryUrlKey = 'cat-1.html'; - $productSku = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - /** @var Product $product */ - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = Bootstrap::getObjectManager()->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $categoryUrlKey, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); $query = $this->getUrlResolverQuery($categoryUrlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cat_c", "cat_c_{$categoryId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForCategory = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCategory] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCategory] + ); //verify cached data is correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('CATEGORY', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('CATEGORY', $cachedResponse['body']['urlResolver']['type']); } + /** - * Test that X-Magento-Tags Cache debug headers are correct for cms page url resolver + * Test cache invalidation for cms page url resolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testUrlResolverCachingForCMSPage() @@ -117,32 +92,34 @@ public function testUrlResolverCachingForCMSPage() /** @var \Magento\Cms\Model\Page $page */ $page = Bootstrap::getObjectManager()->get(\Magento\Cms\Model\Page::class); $page->load('page100'); - $cmsPageId = $page->getId(); $requestPath = $page->getIdentifier(); $query = $this->getUrlResolverQuery($requestPath); - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cms_p", "cms_p_{$cmsPageId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForCmsPage = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCmsPage] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCmsPage] + ); //verify cached data is correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('CMS_PAGE', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('CMS_PAGE', $cachedResponse['body']['urlResolver']['type']); } + /** - * Tests that cache is invalidated when url key is updated and access the original request path + * Tests that cache is invalidated when url key is updated and + * access the original request path * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ public function testCacheIsInvalidatedForUrlResolver() @@ -150,25 +127,34 @@ public function testCacheIsInvalidatedForUrlResolver() $productSku = 'p002'; $urlKey = 'p002.html'; $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - //cache-debug should be a MISS on first request - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - //cache-debug should be a HIT on second request - $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + // Obtain the X-Magento-Cache-Id from the response + $response = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForUrlResolver = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); + //Updating the product url key /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); /** @var Product $product */ $product = $productRepository->get($productSku, false, null, true); $product->setUrlKey('p002-new.html')->save(); - //cache-debug should be a MISS after updating the url key and accessing the same requestPath or urlKey - $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + // Verify we obtain a cache MISS the third time after product url key is updated + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php index 884e2e87a9c57..f47efa6d2ecb8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php @@ -12,11 +12,10 @@ /** * Test coverage for zero subtotal and check/money order payment methods in the store config * - * @magentoDbIsolation enabled */ class StoreConfigTest extends GraphQlAbstract { - const STORE_CONFIG_QUERY = <<<QUERY + public const STORE_CONFIG_QUERY = <<<QUERY { storeConfig { zero_subtotal_enabled diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddProductWithCustomizableOptionToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddProductWithCustomizableOptionToCartTest.php new file mode 100644 index 0000000000000..d247348acfc7a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddProductWithCustomizableOptionToCartTest.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\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test GraphQL can add product with customizable option to cart + */ +class AddProductWithCustomizableOptionToCartTest extends GraphQlAbstract +{ + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'simple'; + $qty = 1; + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']['items']); + self::assertNotEmpty($response['addProductsToCart']['cart']['items'][0]['customizable_options']); + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions = '' + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + items { + product { + name + sku + } + ... on SimpleCartItem { + customizable_options { + label + customizable_option_uid + values { + value + customizable_option_value_uid + } + } + } + } + } + user_errors { + code + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php index feb6dd23e0634..ceb189a7c3b78 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; use Magento\SalesRule\Api\RuleRepositoryInterface; use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule; @@ -17,18 +19,35 @@ use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface as DiscountAppliedTo; /** * Test cases for applying cart promotions to items in cart */ class CartPromotionsTest extends GraphQlAbstract { + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** + * @var float + */ + private const EPSILON = 0.0000000001; + + protected function setUp():void + { + parent::setUp(); + $this->customerAuthenticationHeader = + Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + } + /** * 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']; @@ -151,16 +170,18 @@ public function testCartPromotionsMultipleCartRules() $lineItemDiscount = $productsInResponse[$itemIndex][0]['prices']['discounts']; $expectedTotalDiscountValue = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5) + ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5*0.1); - $this->assertEquals( + $this->assertEqualsWithDelta( $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5, - current($lineItemDiscount)['amount']['value'] + current($lineItemDiscount)['amount']['value'], + self::EPSILON ); $this->assertEquals('TestRule_Label', current($lineItemDiscount)['label']); $lineItemDiscountValue = next($lineItemDiscount)['amount']['value']; - $this->assertEquals( + $this->assertEqualsWithDelta( round($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5)*0.1, - $lineItemDiscountValue + $lineItemDiscountValue, + self::EPSILON ); $this->assertEquals('10% off with two items_Label', end($lineItemDiscount)['label']); $actualTotalDiscountValue = $lineItemDiscount[0]['amount']['value']+$lineItemDiscount[1]['amount']['value']; @@ -180,7 +201,51 @@ public function testCartPromotionsMultipleCartRules() ] ); } - $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 24.18); + $this->assertEquals(21.98, $response['cart']['prices']['discounts'][0]['amount']['value']); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][0][DiscountAppliedTo::APPLIED_TO] + ); + $this->assertEquals($response['cart']['prices']['discounts'][1]['amount']['value'], 2.2); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][1][DiscountAppliedTo::APPLIED_TO], + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @return void + * @throws AuthenticationException + */ + public function testShippingDiscountPresent(): void + { + $skus =['simple1', 'simple2']; + $qty = 2; + $quote = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $this->addMultipleProductsToCustomerCart($cartId, $qty, $skus[0], $skus[1]); + $this->setShippingMethodOnCustomerCart($cartId, ['carrier_code' => 'flatrate', 'method_code' => 'flatrate']); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutationForCustomer($query); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][0][DiscountAppliedTo::APPLIED_TO], + ); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_SHIPPING, + $response['cart']['prices']['discounts'][1][DiscountAppliedTo::APPLIED_TO], + ); } /** @@ -190,6 +255,7 @@ public function testCartPromotionsMultipleCartRules() * Tax rate = 7.5% * Cart rule to apply 50% for products assigned to a specific category * + * @magentoConfigFixture default_store tax/calculation/discount_tax 1 * @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 @@ -490,6 +556,7 @@ private function getCartItemPricesQuery(string $cartId): string prices{ discounts{ amount{value} + applied_to } } } @@ -518,10 +585,11 @@ private function createEmptyCart(): string * @param int $sku1 * @param int $qty * @param string $sku2 + * @return string */ - private function addMultipleSimpleProductsToCart(string $cartId, int $qty, string $sku1, string $sku2): void + private function addSimpleProductsToCartQuery(string $cartId, int $qty, string $sku1, string $sku2): string { - $query = <<<QUERY + return <<<QUERY mutation { addSimpleProductsToCart(input: { cart_id: "{$cartId}", @@ -550,7 +618,17 @@ private function addMultipleSimpleProductsToCart(string $cartId, int $qty, strin } } QUERY; + } + /** + * @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 = $this->addSimpleProductsToCartQuery($cartId, $qty, $sku1, $sku2); $response = $this->graphQlMutation($query); self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); @@ -560,6 +638,74 @@ private function addMultipleSimpleProductsToCart(string $cartId, int $qty, strin self::assertEquals($sku2, $response['addSimpleProductsToCart']['cart']['items'][1]['product']['sku']); } + /** + * Executes GraphQL mutation for a default customer + * + * @param string $query + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function graphQlMutationForCustomer(string $query): array + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + return $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * @param string $cartId + * @param int $sku1 + * @param int $qty + * @param string $sku2 + * @throws AuthenticationException + */ + private function addMultipleProductsToCustomerCart(string $cartId, int $qty, string $sku1, string $sku2): void + { + $query = $this->addSimpleProductsToCartQuery($cartId, $qty, $sku1, $sku2); + $this->graphQlMutationForCustomer($query); + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethodOnCustomerCart(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + + $response = $this->graphQlMutationForCustomer($query); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + /** * Set shipping address for the region for which tax rule is set * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php index ec5b3e92f8283..65589be0a1376 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -131,7 +131,8 @@ public function testGetCartIfCartIdIsEmpty() public function testGetCartIfCartIdIsMissed() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "cart" argument "cart_id" of type "String!" is required but not provided.'); + $message = 'Field "cart" argument "cart_id" of type "String!" is required but not provided.'; + $this->expectExceptionMessage($message); $query = <<<QUERY { @@ -201,7 +202,8 @@ public function testGetCartWithNotDefaultStore() public function testGetCartWithWrongStore() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.'); + $message = 'The account sign-in was incorrect or your account is disabled temporarily.'; + $this->expectExceptionMessage($message.' Please wait and try again later.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery($maskedQuoteId); @@ -238,7 +240,7 @@ public function testGetCartWithNotExistingStore() */ public function testGetCartForLockedCustomer() { - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/750'); + $this->markTestSkipped('https://github.com/magento/graphql-ce/issues/750'); /* lock customer */ $customerSecure = $this->customerRegistry->retrieveSecureData(1); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php index 66e39a7860f39..22a1f55b8265e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store tax/calculation/price_includes_tax 1 * @magentoConfigFixture default_store tax/calculation/shipping_includes_tax 1 * @magentoConfigFixture default_store tax/cart_display/shipping 2 * @magentoConfigFixture default_store tax/classes/shipping_tax_class 2 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php index 870da96ad25f4..b1978964d0d4d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -7,11 +7,11 @@ namespace Magento\GraphQl\Quote\Customer; -use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php index 69dc78b9d08d9..ad0e0049f1883 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -133,6 +133,7 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php index d6daad250a963..9a7b33eb94ed4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -138,6 +138,7 @@ public function testReSetShippingMethod() } /** + * @magentoConfigFixture default_store carriers/freeshipping/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -385,7 +386,9 @@ private function getQuery( public function testSetShippingMethodOnAnEmptyCart() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The shipping method can\'t be set for an empty cart. Add an item to cart and try again.'); + $this->expectExceptionMessage( + 'The shipping method can\'t be set for an empty cart. Add an item to cart and try again.' + ); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $carrierCode = 'flatrate'; 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 d65fb96a7f5b5..cb8fa64bd9cad 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 @@ -65,6 +65,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store checkout/options/guest_checkout 1 * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testCheckoutWorkflow() @@ -168,7 +169,7 @@ private function setGuestEmailOnCart(string $cartId): void private function addProductToCart(string $cartId, float $quantity, string $sku): void { $query = <<<QUERY -mutation { +mutation { addSimpleProductsToCart( input: { cart_id: "{$cartId}" @@ -310,7 +311,7 @@ private function setShippingMethod(string $cartId, array $method): array $query = <<<QUERY mutation { setShippingMethodsOnCart(input: { - cart_id: "{$cartId}", + cart_id: "{$cartId}", shipping_methods: [ { carrier_code: "{$method['carrier_code']}" 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 764fc3f573a2b..097da686d4231 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 @@ -7,7 +7,13 @@ namespace Magento\GraphQl\Quote\Guest; +use Magento\Framework\Exception\LocalizedException; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask; +use Magento\Quote\Test\Fixture\GuestCart; +use Magento\Quote\Test\Fixture\QuoteIdMask as QuoteIdMaskFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -583,6 +589,99 @@ public function testSetBillingAddressWithSameAsShippingAndMultishipping() $this->graphQlMutation($query); } + /** + * Test graphql mutation setting middlename, prefix, suffix and fax in billing address + * + * @throws LocalizedException + */ + #[ + DataFixture(GuestCart::class, as: 'quote'), + DataFixture(QuoteIdMaskFixture::class, ['cart_id' => '$quote.id$'], as: 'mask'), + ] + public function testSetMiddlenamePrefixSuffixFaxBillingAddress() + { + /** @var QuoteIdMask $quoteMask */ + $quoteMask = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage()->get('mask'); + + $expectedResult = [ + 'setBillingAddressOnCart' => [ + 'cart' => [ + 'billing_address' => [ + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'middlename' => 'test middlename', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'fax' => '5552224455', + 'company' => 'test company', + 'street' => [ + 'test street 1', + 'test street 2', + ], + 'city' => 'test city', + 'postcode' => '887766', + 'telephone' => '88776655', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + '__typename' => 'BillingCartAddress' + ] + ] + ] + ]; + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$quoteMask->getMaskedId()}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AL" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + } + } + ) { + cart { + billing_address { + firstname + lastname + middlename + prefix + suffix + fax + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + $this->assertEquals($expectedResult, $response); + } + /** * Verify all the whitelisted fields for a New Address Object * 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 78691d8cbd889..41e58cf59c591 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 @@ -200,6 +200,7 @@ public function testSetPaymentMethodToCustomerCart() } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php index c40a2b9426fe0..6312ad2513e02 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -224,6 +224,7 @@ public function testReSetPayment() } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @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 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php index 121b04cc8ed11..3bfa398b51176 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -124,6 +124,7 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @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 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 b7ddd085f932e..016d200513999 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 @@ -7,7 +7,13 @@ namespace Magento\GraphQl\Quote\Guest; +use Magento\Framework\Exception\LocalizedException; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask; +use Magento\Quote\Test\Fixture\GuestCart; +use Magento\Quote\Test\Fixture\QuoteIdMask as QuoteIdMaskFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -371,6 +377,10 @@ public function testSetShippingAddressWithLowerCaseCountry() address: { firstname: "John" lastname: "Doe" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" street: ["6161 West Centinella Avenue"] city: "Culver City" region: "CA" @@ -423,6 +433,10 @@ public function testSetShippingAddressWithLowerCaseRegion() address: { firstname: "John" lastname: "Doe" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" street: ["6161 West Centinella Avenue"] city: "Culver City" region: "ca" @@ -456,6 +470,101 @@ public function testSetShippingAddressWithLowerCaseRegion() $this->assertEquals('CA', $address['region']['code']); } + /** + * Test graphql mutation setting middlename, prefix, suffix and fax in shipping address + * + * @throws LocalizedException + */ + #[ + DataFixture(GuestCart::class, as: 'quote'), + DataFixture(QuoteIdMaskFixture::class, ['cart_id' => '$quote.id$'], as: 'mask'), + ] + public function testSetMiddlenamePrefixSuffixFaxShippingAddress() + { + /** @var QuoteIdMask $quoteMask */ + $quoteMask = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage()->get('mask'); + + $expectedResult = [ + 'setShippingAddressesOnCart' => [ + 'cart' => [ + 'shipping_addresses' => [ + [ + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'middlename' => 'test middlename', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'fax' => '5552224455', + 'company' => 'test company', + 'street' => [ + 'test street 1', + 'test street 2', + ], + 'city' => 'test city', + 'postcode' => '887766', + 'telephone' => '88776655', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + '__typename' => 'ShippingCartAddress' + ] + ] + ] + ] + ]; + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$quoteMask->getMaskedId()}" + shipping_addresses: { + address: { + firstname: "test firstname" + lastname: "test lastname" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AL" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + } + } + ) { + cart { + shipping_addresses { + firstname + lastname + middlename + prefix + suffix + fax + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + $this->assertEquals($expectedResult, $response); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php index af5aba50f6540..2d8a2310eb48e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -146,6 +146,7 @@ public function testReSetShippingMethod() } /** + * @magentoConfigFixture default_store carriers/freeshipping/active 0 * @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 @@ -164,7 +165,7 @@ public function testSetShippingMethodWithWrongParameters(string $input, string $ $query = <<<QUERY mutation { setShippingMethodsOnCart(input: { - {$input} + {$input} }) { cart { shipping_addresses { @@ -255,7 +256,7 @@ public function testSetMultipleShippingMethods() $query = <<<QUERY mutation { setShippingMethodsOnCart(input: { - cart_id: "{$maskedQuoteId}", + cart_id: "{$maskedQuoteId}", shipping_methods: [ { carrier_code: "flatrate" @@ -317,7 +318,9 @@ public function testSetShippingMethodToCustomerCart() public function testSetShippingMethodOnAnEmptyCart() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The shipping method can\'t be set for an empty cart. Add an item to cart and try again.'); + $this->expectExceptionMessage( + 'The shipping method can\'t be set for an empty cart. Add an item to cart and try again.' + ); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $carrierCode = 'flatrate'; @@ -344,9 +347,9 @@ private function getQuery( ): string { return <<<QUERY mutation { - setShippingMethodsOnCart(input: + setShippingMethodsOnCart(input: { - cart_id: "$maskedQuoteId", + cart_id: "$maskedQuoteId", shipping_methods: [{ carrier_code: "$shippingCarrierCode" method_code: "$shippingMethodCode" 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 8575f1d33c435..2bcee4e168511 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -7,6 +7,8 @@ namespace Magento\GraphQl\RelatedProduct; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -226,4 +228,81 @@ public function testQueryDisableRelatedProductInStore(): void $relatedProducts = $response['products']['items'][0]['related_products']; self::assertCount(0, $relatedProducts); } + #[ + DataFixture(ProductFixture::class, ['name' =>'Simple related product', 'sku' => 'simple_related_product', + 'price' => 20], 'p1'), + DataFixture(ProductFixture::class, ['name' =>'Product as a related product', + 'sku' => 'product_as_a_related_product', 'price' => 30], 'p2'), + DataFixture(ProductFixture::class, ['name' =>'Simple product', 'sku' => 'simple_product', 'price' => 40], 'p3'), + DataFixture(ProductFixture::class, ['name' => 'Simple with related product', + 'sku' =>'simple_with_related_product ', 'price' => 100, + 'product_links' => ['$p3.sku$','$p1.sku$','$p2.sku$' ]], 'p1'), + + ] + public function testQueryRelatedProductsInSortOrder() + { + $productSku = 'simple_with_related_product'; + + $query = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + related_products + { + sku + name + url_key + } + } + } + } +QUERY; + $response = $this->graphQlQuery($query); + + 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(3, $relatedProducts); + self::assertRelatedProductsInSortOrder($relatedProducts); + } + + /** + * @param array $relatedProducts + */ + private function assertRelatedProductsInSortOrder(array $relatedProducts): void + { + $expectedData = [ + 'simple_product' => [ + 'name' => 'Simple product', + 'url_key' => 'simple-product', + + ], + 'simple_related_product' => [ + 'name' => 'Simple related product', + 'url_key' => 'simple-related-product', + + ], + 'product_as_a_related_product' => [ + 'name' => 'Product as a related product', + 'url_key' => 'product-as-a-related-product', + + ] + ]; + + foreach ($relatedProducts as $product) { + self::assertArrayHasKey('sku', $product); + self::assertArrayHasKey('name', $product); + self::assertArrayHasKey('url_key', $product); + + self::assertArrayHasKey($product['sku'], $expectedData); + $productExpectedData = $expectedData[$product['sku']]; + + self::assertEquals($product['name'], $productExpectedData['name']); + self::assertEquals($product['url_key'], $productExpectedData['url_key']); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php index 84388cd98b4bd..f28399fbcde45 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php @@ -7,10 +7,13 @@ namespace Magento\GraphQl\Sales; +use Magento\Customer\Model\ResourceModel\CustomerRepository; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteRepository; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -185,6 +188,12 @@ public function testReorderWithLowStock() $expectedResponse['cart']['items'][0]['quantity'] = 20; $this->assertResponseFields($response['reorderItems'], $expectedResponse); + $customer = ObjectManager::getInstance()->get(CustomerRepository::class) + ->get(self::CUSTOMER_EMAIL); + $quoteRepository = ObjectManager::getInstance()->get(QuoteRepository::class); + $quote = $quoteRepository->getActiveForCustomer($customer->getId()); + $quote->setIsActive(false); + $quoteRepository->save($quote); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php index b54ca217cabf1..b140aab0734fa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -28,6 +28,8 @@ use Magento\Quote\Test\Fixture\CustomerCart; use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Tax\Model\Config as TaxConfig; +use Magento\TestFramework\Fixture\Config; /** * Class RetrieveOrdersTest @@ -107,6 +109,9 @@ public function testGetCustomerOrdersSimpleProductQuery() $this->assertEquals($expectedOrderTotal, $actualOrderTotalFromResponse, 'Totals do not match'); } + #[ + Config(TaxConfig::XML_PATH_DISPLAY_SALES_PRICE, TaxConfig::DISPLAY_TYPE_INCLUDING_TAX), + ] /** * Verify the customer order with tax, discount with shipping tax class set for calculation setting * @@ -168,6 +173,8 @@ public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() ] ]; $this->assertResponseFields($customerOrderResponse[0]["payment_methods"], $paymentMethodAssertionMap); + $this->assertEquals(10.75, $customerOrderResponse[0]['items'][0]['product_sale_price']['value']); + $this->assertEquals(7.5, $customerOrderResponse[0]['total']['taxes'][0]['rate']); // Asserting discounts on order item level $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); @@ -1393,7 +1400,13 @@ private function getCustomerOrderQuery($orderNumber): array billing_address { ... address } - items{product_name product_sku quantity_ordered discounts {amount{value currency} label}} + items{ + product_name + product_sku + quantity_ordered + product_sale_price {value} + discounts {amount{value currency} label} + } total { base_grand_total{value currency} grand_total{value currency} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php new file mode 100644 index 0000000000000..d414a464daf53 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php @@ -0,0 +1,2797 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Store; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\Store\Model\Group; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\Website; +use Magento\TestFramework\App\ApiMutableScopeConfig; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test availableStores query cache + */ +class AvailableStoresCacheTest extends GraphQLPageCacheAbstract +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ApiMutableScopeConfig + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var array + */ + private $origConfigs = []; + + /** + * @var array + */ + private $notExistingOrigConfigs = []; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->config = $this->objectManager->get(ApiMutableScopeConfig::class); + } + + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + */ + public function testAvailableStoreConfigs(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('availableStores', $defaultStoreResponse['body']); + $this->assertCount(1, $defaultStoreResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('availableStores', $defaultStoreResponseHit['body']); + $this->assertCount(1, $defaultStoreResponseHit['body']['availableStores']); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreResponse['body']); + $this->assertCount(2, $secondStoreResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $secondStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreResponseHit['body']); + $this->assertCount(2, $secondStoreResponseHit['body']['availableStores']); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreCurrentStoreGroupResponse = $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreCurrentStoreGroupResponse['body']); + $this->assertCount(1, $secondStoreCurrentStoreGroupResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $secondStoreCurrentStoreGroupResponseHit = $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreCurrentStoreGroupResponseHit['body']); + $this->assertCount(1, $secondStoreCurrentStoreGroupResponseHit['body']['availableStores']); + } + + /** + * Store scoped config change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_STORE, 'third_store_view'); + + // Query available stores of default store's website after 3rd store configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_STORE, $secondStoreCode); + + // Query available stores of default store's website after 2nd store configuration is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store configuration is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store configuration is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Website scope config change triggers purging only the cache of the stores associated with the changed website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second website locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_WEBSITES, 'second'); + + // Query available stores of default store's website after second website configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second website configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second website configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Default scope config change triggers purging the cache of all stores. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithDefaultScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change default locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + + // Query available stores of default store's website after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store name + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load('third_store_view', 'code'); + $thirdStoreName = 'Third Store View'; + $thirdStoreNewName = $thirdStoreName . ' 2'; + $store->setName($thirdStoreNewName); + $store->save(); + + // Query available stores of default store's website after 3rd store is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store name + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load($secondStoreCode, 'code'); + $secondStoreName = 'Second Store View'; + $secondStoreNewName = $secondStoreName . ' 2'; + $store->setName($secondStoreNewName); + $store->save(); + + // Query available stores of default store's website after 2nd store is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store group change triggers purging only the cache of the stores associated with the changed store group. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreGroupChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store group name + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('third_store', 'code'); + $thirdStoreGroupName = 'Third store group'; + $thirdStoreGroupNewName = $thirdStoreGroupName . ' 2'; + $storeGroup->setName($thirdStoreGroupNewName); + $storeGroup->save(); + + // Query available stores of default store's website after 3rd store group is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store group is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store group is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store group name + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + $secondStoreGroupName = 'Second store group'; + $secondStoreGroupNewName = $secondStoreGroupName . ' 2'; + $storeGroup->setName($secondStoreGroupNewName); + $storeGroup->save(); + + // Query available stores of default store's website after 2nd store group is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store website change triggers purging only the cache of the stores associated with the changed store website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store website name + /** @var Website $website */ + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + $secondStoreWebsiteName = 'Second Test Website'; + $secondStoreWebsiteNewName = $secondStoreWebsiteName . ' 2'; + $website->setName($secondStoreWebsiteNewName); + $website->save(); + + // Query available stores of default store's website after second website is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second website is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second website is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store group switches from one website to another website triggers purging the cache of the stores + * associated with both websites. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedAfterStoreGroupSwitchedWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStoreCurrentStoreGroup['headers']); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website and any store groups of the website + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseThirdStore['headers']); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website and store group + $responseThirdStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseThirdStoreCurrentStoreGroup['headers'] + ); + $thirdStoreCurrentStoreGroupCacheId = + $responseThirdStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Second store group switches from second website to base website + /** @var Website $website */ + $website = $this->objectManager->create(Website::class); + $website->load('base', 'code'); + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + $storeGroup->setWebsiteId($website->getId()); + $storeGroup->save(); + + // Query available stores of default store's website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website (second website) and any store groups of the website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website (second website) and store group + // after second store group switched from second website to base website + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Store switches from one store group to another store group triggers purging the cache of the stores + * associated with both store groups. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedAfterStoreSwitchedStoreGroup(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStoreCurrentStoreGroup['headers']); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website and any store groups of the website + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseThirdStore['headers']); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website and store group + $responseThirdStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseThirdStoreCurrentStoreGroup['headers'] + ); + $thirdStoreCurrentStoreGroupCacheId = + $responseThirdStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Second store switches from second store group to main_website_store store group + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('main_website_store', 'code'); + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load($secondStoreCode, 'code'); + $store->setStoreGroupId($storeGroup->getId()); + $store->setWebsiteId($storeGroup->getWebsiteId()); + $store->save(); + + // Query available stores of default store's website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website (second website) and any store groups of the website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website (second website) and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Creating new store with new website and new store group will not purge the cache of the other stores that are not + * associated with the new website and new store group + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewStoreWithNewStoreGroupNewWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new website + $website = $this->objectManager->create(Website::class); + $website->setData([ + 'code' => 'new', + 'name' => 'New Test Website', + 'is_default' => '0', + ]); + $website->save(); + + // Query available stores of default store's website after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Query available stores of default store's website after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new store with new store group and new website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group, new website + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $website->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with new website and second store group will not purge the cache of the other stores that are + * not associated with the new website + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewStoreWithSecondStoreGroupNewWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new website + $website = $this->objectManager->create(Website::class); + $website->setData([ + 'code' => 'new', + 'name' => 'New Test Website', + 'is_default' => '0', + ]); + $website->save(); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new store with second store group and new website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with new website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with new website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with new website and seond store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new website + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $website->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with second website and new store group will only purge the cache of availableStores for + * all stores of second website + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreWithNewStoreGroupSecondWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new store with new store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with second website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new inactive store with second website and new store group will not purge the cache of availableStores + * for all stores of second website, will purge the cache of availableStores for all stores of second website when + * the new store is activated + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewInactiveStoreWithNewStoreGroupSecondWebsitePurgedWhenActivated(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new inactive store with new store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 0, + ]); + $store->save(); + + // Query available stores of default store's website + // after new inactive store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new inactive store with second website and new store group is created + // Verify we obtain a cache Hit at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new inactive store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Activate the store + $store->setIsActive(1); + $store->save(); + + // Query available stores of default store's website after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with second website and second store group will only purge the cache of availableStores for + * all stores of second website or second website with second store group + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreWithSecondStoreGroupSecondWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new store with second store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with second website and second store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with second website and second store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new inactive store with second website and second store group will not purge the cache of + * availableStores for all stores of second website or second website with second store group, will purge the + * cache of availableStores for all stores of second website or second website with second store group + * after the store is activated + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewInactiveStoreWithSecondStoreGroupSecondWebsitePurgedAfterActivated(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new inactive store with second store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 0, + ]); + $store->save(); + + // Query available stores of default store's website + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Activate the store + $store->setIsActive(1); + $store->save(); + + // Query available stores of default store's website after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with one store group website will purge the cache of availableStores + * no matter for current store group or not + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreCreatedInOneStoreGroupWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseDefaultStoreCurrentStoreGroup['headers'] + ); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get base website + $website = $this->objectManager->create(Website::class); + $website->load('base', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new store with new store group and base website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with default website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after new store with base website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with base website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with base website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + private function changeToTwoWebsitesThreeStoreGroupsThreeStores() + { + /** @var $website2 \Magento\Store\Model\Website */ + $website2 = $this->objectManager->create(Website::class); + $website2Id = $website2->load('second', 'code')->getId(); + + // Change third store to the same website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3GroupId = $store3->getStoreGroupId(); + /** @var Group $store3Group */ + $store3Group = $this->objectManager->create(Group::class); + $store3Group->load($store3GroupId)->setWebsiteId($website2Id)->save(); + $store3->setWebsiteId($website2Id)->save(); + } + + /** + * Get query + * + * @param string $useCurrentGroup + * @return string + */ + private function getQuery(string $useCurrentGroup = ''): string + { + $useCurrentGroupArg = $useCurrentGroup === '' ? '' : '(useCurrentGroup:' . $useCurrentGroup . ')'; + return <<<QUERY +{ + availableStores{$useCurrentGroupArg} { + id, + code, + store_code, + store_name, + store_sort_order, + is_default_store, + store_group_code, + store_group_name, + is_default_store_group, + website_id, + website_code, + website_name, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + use_store_in_url + } +} +QUERY; + } + + protected function tearDown(): void + { + $this->restoreConfig(); + parent::tearDown(); + } + + /** + * Set configuration + * + * @param string $path + * @param string $value + * @param string $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + string $path, + string $value, + string $scopeType, + ?string $scopeCode = null + ): void { + if ($this->configStorage->checkIsRecordExist($path, $scopeType, $scopeCode)) { + $this->origConfigs[] = [ + 'path' => $path, + 'value' => $this->configStorage->getValueFromDb($path, $scopeType, $scopeCode), + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } else { + $this->notExistingOrigConfigs[] = [ + 'path' => $path, + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } + $this->config->setValue($path, $value, $scopeType, $scopeCode); + } + + private function restoreConfig() + { + foreach ($this->origConfigs as $origConfig) { + $this->config->setValue( + $origConfig['path'], + $origConfig['value'], + $origConfig['scopeType'], + $origConfig['scopeCode'] + ); + } + $this->origConfigs = []; + + foreach ($this->notExistingOrigConfigs as $notExistingOrigConfig) { + $this->configStorage->deleteConfigFromDb( + $notExistingOrigConfig['path'], + $notExistingOrigConfig['scopeType'], + $notExistingOrigConfig['scopeCode'] + ); + } + $this->notExistingOrigConfigs = []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php similarity index 90% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php index 013d8d5e40003..b2aa977bbb59f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php @@ -18,7 +18,7 @@ /** * Test the GraphQL endpoint's AvailableStores query */ -class AvailableStoreConfigTest extends GraphQlAbstract +class AvailableStoresTest extends GraphQlAbstract { /** @@ -47,24 +47,14 @@ protected function setUp(): void } /** - * @magentoApiDataFixture Magento/Store/_files/store.php - * @magentoApiDataFixture Magento/Store/_files/inactive_store.php + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php */ - public function testDefaultWebsiteAvailableStoreConfigs(): void + public function testNonDefaultWebsiteAvailableStoreConfigs(): void { - $storeConfigs = $this->storeConfigManager->getStoreConfigs(); - - $expectedAvailableStores = []; - $expectedAvailableStoreCodes = [ - 'default', - 'test' - ]; - - foreach ($storeConfigs as $storeConfig) { - if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { - $expectedAvailableStores[] = $storeConfig; - } - } + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); $query = <<<QUERY @@ -100,20 +90,37 @@ public function testDefaultWebsiteAvailableStoreConfigs(): void } } QUERY; - $response = $this->graphQlQuery($query); + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); $this->assertArrayHasKey('availableStores', $response); - foreach ($expectedAvailableStores as $key => $storeConfig) { + foreach ($storeConfigs as $key => $storeConfig) { $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); } } /** - * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Store/_files/inactive_store.php */ - public function testNonDefaultWebsiteAvailableStoreConfigs(): void + public function testDefaultWebsiteAvailableStoreConfigs(): void { - $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); + $storeConfigs = $this->storeConfigManager->getStoreConfigs(); + + $expectedAvailableStores = []; + $expectedAvailableStoreCodes = [ + 'default', + 'test' + ]; + + foreach ($storeConfigs as $storeConfig) { + if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { + $expectedAvailableStores[] = $storeConfig; + } + } $query = <<<QUERY @@ -149,11 +156,10 @@ public function testNonDefaultWebsiteAvailableStoreConfigs(): void } } QUERY; - $headerMap = ['Store' => 'fixture_second_store']; - $response = $this->graphQlQuery($query, [], '', $headerMap); + $response = $this->graphQlQuery($query); $this->assertArrayHasKey('availableStores', $response); - foreach ($storeConfigs as $key => $storeConfig) { + foreach ($expectedAvailableStores as $key => $storeConfig) { $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); } } @@ -206,6 +212,9 @@ private function validateStoreConfig(StoreConfigInterface $storeConfig, array $r } /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php * @magentoConfigFixture web/url/use_store 1 */ @@ -266,6 +275,9 @@ public function testAllStoreConfigsWithCodeInUrlEnabled(): void } /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php */ public function testCurrentGroupStoreConfigs(): void 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 c79bbf0e0a300..80f55d83a40e2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -35,6 +35,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 * @magentoApiDataFixture Magento/Store/_files/store.php * @throws NoSuchEntityException */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php new file mode 100644 index 0000000000000..357aba87db9b8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\SwatchesGraphQl; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Test\Fixture\Attribute; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test catalog EAV attributes metadata retrieval via GraphQL API + */ +#[ + DataFixture( + Attribute::class, + [ + 'frontend_input' => 'multiselect', + 'is_filterable_in_search' => true, + 'position' => 6, + 'additional_data' => + '{"swatch_input_type":"visual","update_product_preview_image":1,"use_product_image_for_swatch":0}' + ], + 'product_attribute' + ), +] +class AttributesMetadataTest extends GraphQlAbstract +{ + private const QUERY = <<<QRY +{ + customAttributeMetadataV2(attributes: [{attribute_code: "%s", entity_type: "catalog_product"}]) { + items { + code + label + entity_type + frontend_input + is_required + default_value + is_unique + ...on CatalogAttributeMetadata { + is_filterable_in_search + is_searchable + is_filterable + is_comparable + is_html_allowed_on_front + is_used_for_price_rules + is_wysiwyg_enabled + is_used_for_promo_rules + used_in_product_listing + apply_to + swatch_input_type + update_product_preview_image + use_product_image_for_swatch + } + } + errors { + type + message + } + } +} +QRY; + + /** + * @return void + * @throws \Exception + */ + public function testMetadataProduct(): void + { + /** @var ProductAttributeInterface $productAttribute */ + $productAttribute = DataFixtureStorageManager::getStorage()->get('product_attribute'); + + $result = $this->graphQlQuery( + sprintf( + self::QUERY, + $productAttribute->getAttributeCode() + ) + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $productAttribute->getAttributeCode(), + 'label' => $productAttribute->getDefaultFrontendLabel(), + 'entity_type' => strtoupper(ProductAttributeInterface::ENTITY_TYPE_CODE), + 'frontend_input' => 'MULTISELECT', + 'is_required' => false, + 'default_value' => $productAttribute->getDefaultValue(), + 'is_unique' => false, + 'is_filterable_in_search' => true, + 'is_searchable' => false, + 'is_filterable' => false, + 'is_comparable' => false, + 'is_html_allowed_on_front' => true, + 'is_used_for_price_rules' => false, + 'is_wysiwyg_enabled' => false, + 'is_used_for_promo_rules' => false, + 'used_in_product_listing' => false, + 'apply_to' => null, + 'swatch_input_type' => 'VISUAL', + 'update_product_preview_image' => true, + 'use_product_image_for_swatch' => false + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php index 173e817c94c4a..41b03933f40a2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -19,8 +19,8 @@ */ class ShipOrderTest extends \Magento\TestFramework\TestCase\WebapiAbstract { - const SERVICE_READ_NAME = 'salesShipOrderV1'; - const SERVICE_VERSION = 'V1'; + public const SERVICE_READ_NAME = 'salesShipOrderV1'; + public const SERVICE_VERSION = 'V1'; /** * @var ObjectManagerInterface @@ -52,7 +52,7 @@ protected function setUp(): void */ public function testConfigurableShipOrder() { - $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/1335'); + $this->markTestSkipped('https://github.com/magento-engcom/msi/issues/1335'); $productsQuantity = 1; /** @var Order $existingOrder */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppIsolation.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppIsolation.php index 859e2c5584e0b..d6571f530d8f8 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppIsolation.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppIsolation.php @@ -90,7 +90,7 @@ public function endTest(TestCase $test) $values = $this->parse($test); } catch (\Throwable $exception) { ExceptionHandler::handle( - 'Unable to parse fixtures', + 'Unable to parse annotations', get_class($test), $test->getName(false), $exception diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php b/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php index 2add2ed48fb98..419cbde64d9b7 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php @@ -87,6 +87,7 @@ protected function _processTransactionRequests($eventName, \PHPUnit\Framework\Te * * @param \PHPUnit\Framework\TestCase $test * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _startTransaction(\PHPUnit\Framework\TestCase $test) { @@ -110,6 +111,7 @@ function ($errNo, $errStr, $errFile, $errLine) use ($test) { $this->_eventManager->fireEvent('startTransaction', [$test]); restore_error_handler(); } catch (\Exception $e) { + $this->_isTransactionActive = false; $test->getTestResultObject()->addFailure( $test, new \PHPUnit\Framework\AssertionFailedError((string)$e), @@ -125,8 +127,8 @@ function ($errNo, $errStr, $errFile, $errLine) use ($test) { protected function _rollbackTransaction() { if ($this->_isTransactionActive) { - $this->_getConnection()->rollbackTransparentTransaction(); $this->_isTransactionActive = false; + $this->_getConnection()->rollbackTransparentTransaction(); $this->_eventManager->fireEvent('rollbackTransaction'); $this->_getConnection()->closeConnection(); } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php b/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php index 7c076452ea7fa..eab1cb8646a00 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php @@ -95,7 +95,16 @@ private function convertCustomAttributesToMap(array $data): array // check if data is not an associative array if (array_values($data) === $data) { foreach ($data as $item) { - $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = $item[AttributeInterface::VALUE]; + if (isset($item[AttributeInterface::VALUE])) { + $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = $item[AttributeInterface::VALUE]; + } elseif (isset($item['selected_options'])) { + $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = implode( + ',', + array_map(function ($option): string { + return $option[AttributeInterface::VALUE] ?? ''; + }, $item['selected_options']) + ); + } } } else { $result = $data; diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 07008d79218cd..b13ad62c95a62 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -107,7 +107,7 @@ protected function tearDown(): void public function testAclHasAccess() { if ($this->uri === null) { - $this->markTestIncomplete('AclHasAccess test is not complete'); + $this->markTestSkipped('AclHasAccess test is not complete'); } if ($this->httpMethod) { $this->getRequest()->setMethod($this->httpMethod); @@ -123,7 +123,7 @@ public function testAclHasAccess() public function testAclNoAccess() { if ($this->resource === null || $this->uri === null) { - $this->markTestIncomplete('Acl test is not complete'); + $this->markTestSkipped('Acl test is not complete'); } if ($this->httpMethod) { $this->getRequest()->setMethod($this->httpMethod); diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php index 091bf25f24265..ef81c45097655 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php @@ -73,7 +73,6 @@ public function testDisable() $this->checkInitialStatus(); $this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::DISABLE_VALUE); $this->reinitableConfig->reinit(); - $this->checkDisabledStatus(); } @@ -83,8 +82,8 @@ public function testDisable() */ public function testReEnable() { - $this->checkDisabledStatus(); $this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::ENABLE_VALUE); + $this->reinitableConfig->reinit(); $this->checkReEnabledStatus(); } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php index 852308e83c270..3d5d980364132 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php @@ -7,9 +7,6 @@ use Magento\TestFramework\Helper\Bootstrap; -/** - * Class SearchResultTest - */ class SearchResultTest extends \PHPUnit\Framework\TestCase { /** @@ -29,6 +26,6 @@ public function testGetAllIds() $searchResult = $objectManager->create( \Magento\AsynchronousOperations\Ui\Component\DataProvider\SearchResult::class ); - $this->assertEquals(5, $searchResult->getTotalCount()); + $this->assertEquals(6, $searchResult->getTotalCount()); } } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php index 576927184ba8a..e62e4ed8247e0 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php @@ -29,6 +29,13 @@ 'description' => 'Bulk Description', 'operation_count' => 3, ], + 'in_progress_integration' => [ + 'uuid' => 'bulk-uuid-2.1', + 'user_id' => 100, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_INTEGRATION, + 'description' => 'Bulk Description', + 'operation_count' => 3, + ], 'in_progress_failed' => [ 'uuid' => 'bulk-uuid-3', 'user_id' => 1, diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php new file mode 100644 index 0000000000000..857a9ceb1c240 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php @@ -0,0 +1,101 @@ +<?php +declare(strict_types=1); + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Block\Widget\Grid\Column\Renderer; + +use Magento\Backend\Block\Widget\Grid\Column; +use Magento\Framework\DataObject; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +class ActionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var RendererInterface + */ + private $origRenderer; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->origRenderer = Phrase::getRenderer(); + /** @var RendererInterface|PHPUnit\Framework\MockObject_MockObject $rendererMock */ + $rendererMock = $this->getMockForAbstractClass(RendererInterface::class); + $rendererMock->expects($this->any()) + ->method('render') + ->willReturnCallback( + function ($input) { + return end($input) . ' translated'; + } + ); + Phrase::setRenderer($rendererMock); + } + + protected function tearDown(): void + { + Phrase::setRenderer($this->origRenderer); + } + + /** + * @param array $columnData + * @param array $rowData + * @param string $expected + * @dataProvider renderDataProvider + */ + public function testRender($columnData, $rowData, $expected) + { + /** @var Text $renderer */ + $renderer = $this->objectManager->create(Action::class); + /** @var Column $column */ + $column = $this->objectManager->create( + Column::class, + [ + 'data' => $columnData + ] + ); + /** @var DataObject $row */ + $row = $this->objectManager->create( + DataObject::class, + [ + 'data' => $rowData + ] + ); + $this->assertStringContainsString( + $expected, + $renderer->setColumn($column)->render($row) + ); + } + + /** + * @return array + */ + public function renderDataProvider(): array + { + return [ + [ + [ + 'index' => 'type', + 'type' => 'action', + 'actions' => [ + 'rollback_action'=> [ + 'caption' => 'Rollback', 'href'=>'#', 'onclick' => 'alert("test")' + ] + ] + ], + [], + 'alert("test")' + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php index 517109625424c..e6f351403a77c 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php @@ -97,7 +97,7 @@ public function testGetJsonConfig(): void public function testStockStatusView(bool $isSalable, string $expectedValue): void { $product = $this->productRepository->get('bundle-product'); - $product->setAllItemsSalable($isSalable); + $product->setIsSalable($isSalable); $this->block->setTemplate('Magento_Bundle::catalog/product/view/type/bundle.phtml'); $result = $this->renderBlockHtml($product); $this->assertEquals($expectedValue, trim(strip_tags($result))); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php index 2ae79f07cde6a..f7000c45c3cb7 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php @@ -42,8 +42,8 @@ public function testIsSaleableOnEnabledStatus() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if his status is enabled' + 'Bundle product supposed to be saleable' + . ' if his status is enabled' ); } @@ -60,8 +60,8 @@ public function testIsSaleableOnDisabledStatus() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if his status is disabled' + 'Bundle product supposed to be non saleable' + . ' if his status is disabled' ); } @@ -80,8 +80,8 @@ public function testIsSaleableOnEnabledStatusAndIsSalableIsTrue() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if his status is enabled and it has data is_salable = true' + 'Bundle product supposed to be saleable' + . ' if his status is enabled and it has data is_salable = true' ); } @@ -100,44 +100,8 @@ public function testIsSaleableOnEnabledStatusAndIsSalableIsFalse() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if his status is enabled but his data is_salable = false' - ); - } - - /** - * Check bundle product is saleable if it has all_items_salable = true - * - * @magentoAppIsolation enabled - * @covers \Magento\Bundle\Model\Product\Type::isSalable - */ - public function testIsSaleableOnAllItemsSalableIsTrue() - { - $bundleProduct = $this->productRepository->get('bundle-product'); - $bundleProduct->setData('all_items_salable', true); - - $this->assertTrue( - $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if it has data all_items_salable = true' - ); - } - - /** - * Check bundle product is NOT saleable if it has all_items_salable = false - * - * @magentoAppIsolation enabled - * @covers \Magento\Bundle\Model\Product\Type::isSalable - */ - public function testIsSaleableOnAllItemsSalableIsFalse() - { - $bundleProduct = $this->productRepository->get('bundle-product'); - $bundleProduct->setData('all_items_salable', false); - - $this->assertFalse( - $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has data all_items_salable = false' + 'Bundle product supposed to be non saleable' + . ' if his status is enabled but his data is_salable = false' ); } @@ -164,8 +128,8 @@ public function testIsSaleableOnBundleWithoutOptions() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has no options' + 'Bundle product supposed to be non saleable' + . ' if it has no options' ); } @@ -194,8 +158,8 @@ public function testIsSaleableOnBundleWithoutSelections() $bundleProduct = $this->productRepository->get('bundle-product', false, null, true); $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has no selections' + 'Bundle product supposed to be non saleable' + . ' if it has no selections' ); } @@ -219,8 +183,8 @@ public function testIsSaleableOnBundleWithoutSaleableSelections() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if all his selections are not saleable' + 'Bundle product supposed to be non saleable' + . ' if all his selections are not saleable' ); } @@ -244,8 +208,8 @@ public function testIsSaleableOnBundleWithoutSaleableSelectionsOnRequiredOption( $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has at least one required option with no saleable selections' + 'Bundle product supposed to be non saleable' + . ' if it has at least one required option with no saleable selections' ); } @@ -264,8 +228,8 @@ public function testIsSaleableOnBundleWithNotEnoughQtyOfSelection() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if there are not enough qty of selections on required options' + 'Bundle product supposed to be non saleable' + . ' if there are not enough qty of selections on required options' ); } @@ -299,8 +263,8 @@ public function testIsSaleableOnBundleWithSelectionCanChangeQty() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if all his selections have selection_can_change_qty = 1' + 'Bundle product supposed to be saleable' + . ' if all his selections have selection_can_change_qty = 1' ); } @@ -336,8 +300,8 @@ public function testIsSaleableOnBundleWithoutRequiredOptions() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be not saleable - if all his options are not required and selections are not saleable' + 'Bundle product supposed to be not saleable' + . ' if all his options are not required and selections are not saleable' ); } @@ -375,8 +339,8 @@ public function testIsSaleableOnBundleWithOneSaleableSelection() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if it has at least one not required option with saleable selection' + 'Bundle product supposed to be saleable' + . ' if it has at least one not required option with saleable selection' ); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php index df8d79c5fff6d..0c8ab6ae8de16 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php @@ -197,7 +197,7 @@ public function testIsSalable( $productLink->setQty($selectionQty); } } - $productRepository->save($bundle); + $bundle = $productRepository->save($bundle); $this->assertEquals($isSalable, $bundle->isSalable()); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php index d3857b2fc0d6e..b97f8ab391e66 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php @@ -6,6 +6,15 @@ namespace Magento\Bundle\Model\ResourceModel\Indexer; +use Magento\Bundle\Test\Fixture\Link as BundleSelectionFixture; +use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture; +use Magento\Bundle\Test\Fixture\Product as BundleProductFixture; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Status as StockStatusResource; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Helper\Bootstrap; + class StockTest extends \PHPUnit\Framework\TestCase { /** @@ -15,7 +24,7 @@ class StockTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $this->processor = Bootstrap::getObjectManager()->get( \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class ); } @@ -29,11 +38,11 @@ public function testReindexAll() { $this->processor->reindexAll(); - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = Bootstrap::getObjectManager()->get( \Magento\Catalog\Block\Product\ListProduct::class ); @@ -63,4 +72,130 @@ public function testReindexAll() $this->assertEquals($expectedResult[$product->getName()], $product->getQty()); } } + + #[ + DataFixture( + ProductFixture::class, + ['sku' => 'simple1', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's1' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple2', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's2' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple3', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's3' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple4', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's4' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s1.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link1' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s2.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link2' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s3.sku$', 'qty' => 2, 'can_change_quantity' => 1], + 'link3' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s4.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link4' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link3$']], 'opt2'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link4$'], 'required' => false], 'opt3'), + DataFixture(BundleProductFixture::class, ['sku' => 'bundle1', '_options' => ['$opt1$', '$opt2$', '$opt3$']]), + ] + /** + * @dataProvider reindexRowDataProvider + * @param array $stockItems + * @param bool $expectedStockStatus + * @return void + */ + public function testReindexRow(array $stockItems, bool $expectedStockStatus): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + foreach ($stockItems as $sku => $stockItem) { + $child = $productRepository->get($sku); + $child->setStockData($stockItem); + $productRepository->save($child); + } + $bundle = $productRepository->get('bundle1'); + $this->processor->reindexRow($bundle->getId()); + + $stockStatusResource = Bootstrap::getObjectManager()->get(StockStatusResource::class); + $stockStatus = $stockStatusResource->getProductsStockStatuses($bundle->getId(), 0); + self::assertEquals($expectedStockStatus, (bool) $stockStatus[$bundle->getId()]); + } + + public function reindexRowDataProvider(): array + { + return [ + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple2' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple2' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + ], + false, + ], + [ + [ + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 0], + ], + false, + ], + [ + [ + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 0], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + 'simple2' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + 'simple3' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + 'simple2' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + 'simple3' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + ], + true, + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php new file mode 100644 index 0000000000000..dfdadd17f60e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\ResourceModel\Option; + +use Magento\Bundle\Test\Fixture\Link as BundleSelectionFixture; +use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture; +use Magento\Bundle\Test\Fixture\Product as BundleProductFixture; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class AreBundleOptionsSalableTest extends TestCase +{ + /** + * @var AreBundleOptionsSalable + */ + private $areBundleOptionsSalable; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + protected function setUp(): void + { + $this->areBundleOptionsSalable = Bootstrap::getObjectManager()->create(AreBundleOptionsSalable::class); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->storeRepository = Bootstrap::getObjectManager()->get(StoreRepositoryInterface::class); + } + + #[ + DbIsolation(false), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$group2.id$', 'code' => 'store2'], 'store2'), + DataFixture(ProductFixture::class, ['sku' => 'simple1', 'website_ids' => [1, '$website2.id']], 's1'), + DataFixture(ProductFixture::class, ['sku' => 'simple2', 'website_ids' => [1, '$website2.id']], 's2'), + DataFixture(ProductFixture::class, ['sku' => 'simple3', 'website_ids' => [1, '$website2.id']], 's3'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s1.sku$'], 'link1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s2.sku$'], 'link2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s3.sku$'], 'link3'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link3$'], 'required' => false], 'opt2'), + DataFixture( + BundleProductFixture::class, + ['sku' => 'bundle1', '_options' => ['$opt1$', '$opt2$'], 'website_ids' => [1, '$website2.id']] + ), + ] + /** + * @dataProvider executeDataProvider + * @param string $storeCodeForChange + * @param array $disabledChildren + * @param string $storeCodeForCheck + * @param bool $expectedResult + * @return void + */ + public function testExecute( + string $storeCodeForChange, + array $disabledChildren, + string $storeCodeForCheck, + bool $expectedResult + ): void { + $storeForChange = $this->storeRepository->get($storeCodeForChange); + foreach ($disabledChildren as $childSku) { + $child = $this->productRepository->get($childSku, true, $storeForChange->getId(), true); + $child->setStatus(ProductStatus::STATUS_DISABLED); + $this->productRepository->save($child); + } + + $bundle = $this->productRepository->get('bundle1'); + $storeForCheck = $this->storeRepository->get($storeCodeForCheck); + $result = $this->areBundleOptionsSalable->execute((int) $bundle->getId(), (int) $storeForCheck->getId()); + self::assertEquals($expectedResult, $result); + } + + public function executeDataProvider(): array + { + return [ + ['default', ['simple1'], 'default', true], + ['default', ['simple3'], 'default', true], + ['default', ['simple1', 'simple2'], 'default', false], + ['default', ['simple1', 'simple2'], 'store2', true], + ['store2', ['simple1', 'simple2', 'simple3'], 'store2', false], + ['store2', ['simple1', 'simple2', 'simple3'], 'default', true], + ['admin', ['simple1', 'simple2'], 'default', false], + ['admin', ['simple1', 'simple2'], 'store2', false], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php index 4526a83bb0bce..af81c060f3cc4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php @@ -9,9 +9,17 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Customer\Model\Group; use Magento\Customer\Model\Session; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Result\PageFactory; +use Magento\Tax\Model\Config as TaxConfig; +use Magento\Tax\Test\Fixture\TaxRate as TaxRateFixture; +use Magento\Tax\Test\Fixture\TaxRule as TaxRuleFixture; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; @@ -56,6 +64,45 @@ protected function setUp(): void parent::setUp(); } + #[ + ConfigFixture(TaxConfig::CONFIG_XML_PATH_PRICE_INCLUDES_TAX, 0, 'store', 'default'), + ConfigFixture(TaxConfig::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, 3, 'store', 'default'), + DataFixture( + TaxRateFixture::class, + as: 'rate' + ), + DataFixture( + TaxRuleFixture::class, + [ + 'customer_tax_class_ids' => [3], + 'product_tax_class_ids' => [2], + 'tax_rate_ids' => ['$rate.id$'] + ], + 'rule' + ), + DataFixture(CategoryFixture::class, as: 'category'), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple-product-tax-both', + 'category_ids' => [1, '$category.id$'], + 'tier_prices' => [ + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 2, + 'value' => 5 + ] + ] + ] + ) + ] + public function testRenderAmountMinimalProductWithTierPricesShouldShowMinTierPriceWithTaxes() + { + $priceHtml = $this->getProductPriceHtml('simple-product-tax-both'); + $this->assertFinalPrice($priceHtml, 10.00); + $this->assertAsLowAsPriceWithTaxes($priceHtml, 5.500001, 5.00); + } + /** * Assert that product price without additional price configurations will render as expected. * @@ -242,6 +289,30 @@ private function assertAsLowAsPrice(string $priceHtml, float $expectedPrice): vo ); } + /** + * Assert that price html contain "As low as" label and expected price amount with taxes + * + * @param string $priceHtml + * @param float $expectedPriceWithTaxes + * @param float $expectedPriceWithoutTaxes + * @return void + */ + private function assertAsLowAsPriceWithTaxes( + string $priceHtml, + float $expectedPriceWithTaxes, + float $expectedPriceWithoutTaxes + ): void { + $this->assertMatchesRegularExpression( + sprintf( + '/<span class="price-label">As low as<\/span>(.)+<span.*data-price-amount="%s".*>\\$%01.2f<\/span>(.)+<span class="price">\$%01.2f<\/span>/',//phpcs:ignore + $expectedPriceWithTaxes, + $expectedPriceWithTaxes, + $expectedPriceWithoutTaxes + ), + $priceHtml + ); + } + /** * Assert that price html contain expected final price amount. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index cd64637bf6ec4..e1a41c665bbf9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -99,7 +99,7 @@ public function testProductListSortOrder( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $sortBy); $this->renderBlock($category, $direction); @@ -122,7 +122,7 @@ public function testProductListSortOrderWithConfig( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $this->assertProductListSortOrderWithConfig($sortBy, $direction, $expectation); } @@ -199,7 +199,7 @@ public function testProductListSortOrderOnStoreView( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $defaultSortBy); @@ -227,7 +227,7 @@ public function testProductListSortOrderWithConfigOnStoreView( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $this->objectManager->removeSharedInstance(Config::class); $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php index 9d3f11eb1247a..390ad01173882 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php @@ -104,6 +104,7 @@ private function prepareAdditionalStore() $storeGroup = $this->objectManager->create(\Magento\Store\Model\Group::class); $storeGroup->setWebsiteId($website->getId()); $storeGroup->setName('Fixture Store Group'); + $storeGroup->setCode('fixturestoregroup'); $storeGroup->setRootCategoryId(2); $storeGroup->setDefaultStoreId($store->getId()); $storeGroup->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index c53ee2170d4b4..6ee6c8a2b7e75 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -28,6 +28,9 @@ class AttributeTest extends AbstractBackendController { /** @var PublisherConsumerController */ private $publisherConsumerController; + /** + * @var string[] + */ private $consumers = ['product_action_attribute.update']; protected function setUp(): void @@ -126,6 +129,7 @@ public function testSaveActionChangeVisibility($attributes) /** @var ListProduct $listProduct */ $listProduct = $this->_objectManager->get(ListProduct::class); + sleep(30); // timeout to processing queue $this->publisherConsumerController->waitForAsynchronousResult( function () use ($repository) { sleep(10); // Should be refactored in the scope of MC-22947 diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php index 957b5e9325da7..eb19b7534b5b0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php @@ -78,6 +78,6 @@ protected function assertAttributeIsDeleted(string $attributeCode): void */ public function testAclHasAccess() { - $this->markTestIncomplete('AclHasAccess test is not complete'); + $this->markTestSkipped('AclHasAccess test is not complete'); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php index 5b9266dc11371..0e454a854d8ff 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php @@ -1,9 +1,9 @@ <?php +declare(strict_types=1); /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Indexer\Product\Flat; @@ -17,6 +17,7 @@ /** * Integration tests for \Magento\Catalog\Model\Indexer\Product\Flat\Processor. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProcessorTest extends TestCase { @@ -145,7 +146,13 @@ public function testAddNewStoreGroup(): void \Magento\Store\Model\Group::class ); $storeGroup->setData( - ['website_id' => 1, 'name' => 'New Store Group', 'root_category_id' => 2, 'group_id' => null] + [ + 'website_id' => 1, + 'name' => 'New Store Group', + 'root_category_id' => 2, + 'group_id' => null, + 'code' => 'newstoregroup' + ] ); $storeGroup->save(); $this->assertTrue($this->processor->getIndexer()->isInvalid()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php index a3b2862aa2d20..47d03741bbd66 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php @@ -93,7 +93,7 @@ protected function _prepareFilter($layer, $priceResource, $request = null) */ public function testWithLimits() { - $this->markTestIncomplete('Bug MAGE-6561'); + $this->markTestSkipped('Bug MAGE-6561'); $layer = $this->createLayer(); $priceResource = $this->createPriceResource($layer); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php index 24d5b668ac09c..e5cc7082b65da 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php @@ -54,7 +54,7 @@ protected function setUp(): void public function testValidate(): void { $this->expectException(LocalizedException::class); - $this->expectErrorMessage((string)__('Please enter a valid number in this field.')); + $this->expectExceptionMessage((string)__('Please enter a valid number in this field.')); $product = $this->productFactory->create(); $product->setQuantityAndStockStatus(['qty' => 'string']); $this->model->validate($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php index 9d388dfac3a9e..5d1fff9f62feb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -136,7 +136,7 @@ public function postRequestData(): array public function testAuthorizedSavingOfWithException(array $data): void { $this->expectException(AuthorizationException::class); - $this->expectErrorMessage('Not allowed to edit the product\'s design attributes'); + $this->expectExceptionMessage('Not allowed to edit the product\'s design attributes'); $this->request->setPost(new Parameters($data)); /** @var Product $product */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php index 11c9c6166e07d..cb698d8fc6591 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php @@ -377,7 +377,7 @@ public function testGetWeight() public function testHasOptions() { - $this->markTestIncomplete('Bug MAGE-2814'); + $this->markTestSkipped('Bug MAGE-2814'); $product = new DataObject(); $this->assertFalse($this->_model->hasOptions($product)); 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 46281e721b076..bb108040fa082 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php @@ -11,7 +11,6 @@ /** * Test class for \Magento\Catalog\Model\Product\Url. * - * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -37,6 +36,9 @@ protected function setUp(): void ); } + /** + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php + */ public function testGetUrlInStore() { $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -89,6 +91,7 @@ public function getUrlsWithSecondStoreProvider() /** * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php */ public function testGetProductUrl() { @@ -99,52 +102,10 @@ public function testGetProductUrl() $this->assertStringEndsWith('simple-product.html', $this->_model->getProductUrl($product)); } - public function testFormatUrlKey() - { - $this->assertEquals('abc-test', $this->_model->formatUrlKey('AbC#-$^test')); - } - - public function testGetUrlPath() - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->setUrlPath('product.html'); - - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class, - ['data' => ['url_path' => 'category', 'entity_id' => 5, 'path_ids' => [2, 3, 5]]] - ); - $category->setOrigData(); - - $this->assertEquals('product.html', $this->urlPathGenerator->getUrlPath($product)); - $this->assertEquals('category/product.html', $this->urlPathGenerator->getUrlPath($product, $category)); - } - - /** - * @magentoDbIsolation disabled - * @magentoAppArea frontend - */ - public function testGetUrl() - { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class - ); - $product = $repository->get('simple'); - $this->assertStringEndsWith('simple-product.html', $this->_model->getUrl($product)); - - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->setId(100); - $this->assertStringContainsString('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 * + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @magentoDbIsolation disabled @@ -187,4 +148,52 @@ public function testGetProductUrlWithRearrangedUrlRewrites() $urlPersist->replace($rewrites); $this->assertStringNotContainsString($category->getUrlPath(), $this->_model->getProductUrl($product)); } + + /** + * @magentoDbIsolation disabled + */ + public function testFormatUrlKey() + { + $this->assertEquals('abc-test', $this->_model->formatUrlKey('AbC#-$^test')); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php + * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + */ + public function testGetUrl() + { + $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class + ); + $product = $repository->get('simple'); + $this->assertStringEndsWith('simple-product.html', $this->_model->getProductUrl($product)); + + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Product::class + ); + $product->setId(100); + $this->assertStringContainsString('catalog/product/view/id/100/', $this->_model->getUrl($product)); + } + + public function testGetUrlPath() + { + /** @var $product \Magento\Catalog\Model\Product */ + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Product::class + ); + $product->setUrlPath('product.html'); + + /** @var $category \Magento\Catalog\Model\Category */ + $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Category::class, + ['data' => ['url_path' => 'category', 'entity_id' => 5, 'path_ids' => [2, 3, 5]]] + ); + $category->setOrigData(); + + $this->assertEquals('product.html', $this->urlPathGenerator->getUrlPath($product)); + $this->assertEquals('category/product.html', $this->urlPathGenerator->getUrlPath($product, $category)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php index 9ae327036971b..f94b9c6db54a3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php @@ -65,7 +65,7 @@ public function testSaveWithoutWebsiteId(): void $productWebsiteLink = $this->productWebsiteLinkFactory->create(); $productWebsiteLink->setSku('unique-simple-azaza'); $this->expectException(InputException::class); - $this->expectErrorMessage((string)__('There are not websites for assign to product')); + $this->expectExceptionMessage((string)__('There are not websites for assign to product')); $this->productWebsiteLinkRepository->save($productWebsiteLink); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php index 9979e8cd6ea68..8c32cb192ad32 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php @@ -32,6 +32,7 @@ * @magentoAppArea adminhtml * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SuffixTest extends TestCase { @@ -83,7 +84,7 @@ protected function setUp(): void public function testSaveWithError(): void { $this->expectException(LocalizedException::class); - $this->expectErrorMessage((string)__('Anchor symbol (#) is not supported in url rewrite suffix.')); + $this->expectExceptionMessage((string)__('Anchor symbol (#) is not supported in url rewrite suffix.')); $this->model->setValue('.html#'); $this->model->beforeSave(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php index 0ed7317762056..e6096877aa721 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php @@ -12,7 +12,9 @@ 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' ); $attribute->load('dropdown_attribute', 'attribute_code'); -$attribute->delete(); +if ($attribute->getAttributeId()) { + $attribute->delete(); +} $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 18fcaff5cc1f6..f48cdc501d392 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -20,6 +20,9 @@ use Magento\CatalogInventory\Model\Stock\Item; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\ImportExport\Api\Data\LocalizedExportInfoInterface; +use Magento\ImportExport\Api\ExportManagementInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Test\Fixture\Store as StoreFixture; @@ -28,6 +31,7 @@ use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Fixture\DbIsolation; +use Magento\Translation\Test\Fixture\Translation; /** * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php @@ -144,6 +148,7 @@ public function testExport(): void /** * Verify successful export of product with stock data with 'use config max sale quantity is enabled * + * @magentoConfigFixture default/cataloginventory/item_options/manage_stock 1 * @magentoDataFixture /Magento/Catalog/_files/product_without_options_with_stock_data.php * @magentoDbIsolation enabled * @return void @@ -162,6 +167,8 @@ public function testExportWithStock(): void $stockItem = $product->getExtensionAttributes()->getStockItem(); $stockItem->setMaxSaleQty($maxSaleQty); $stockItem->setMinSaleQty($minSaleQty); + $stockItem->setManageStock(0); + $stockItem->setUseConfigManageStock(1); $stockRepository->save($stockItem); $this->model->setWriter( @@ -170,10 +177,14 @@ public function testExportWithStock(): void ) ); $exportData = $this->model->export(); + $rows = $this->csvToArray($exportData); + $this->assertStringContainsString((string)$stockConfiguration->getMaxSaleQty(), $exportData); $this->assertStringNotContainsString($maxSaleQty, $exportData); $this->assertStringNotContainsString($minSaleQty, $exportData); $this->assertStringContainsString('Simple Product Without Custom Options', $exportData); + $this->assertEquals(1, $rows[0]['use_config_manage_stock']); + $this->assertEquals(1, $rows[0]['manage_stock']); } /** @@ -879,4 +890,31 @@ public function testExportCategoryPathHasAdminScopeNames(): void $exportData = $this->model->export(); $this->assertStringNotContainsString('NewCategoryName', $exportData); } + + #[ + DataFixture( + Translation::class, + [ + 'string' => 'Catalog, Search', + 'translate' => 'Katalog, Suche', + 'locale' => 'de_DE', + ] + ), + DataFixture(ProductFixture::class, as: 'p1') + ] + public function testExportWithSpecificLocale(): void + { + $sku = $this->fixtures->get('p1')->getSku(); + $exportFilter = [ + 'sku' => $sku + ]; + $exportManager = $this->objectManager->get(ExportManagementInterface::class); + $exportInfo = $this->objectManager->create(LocalizedExportInfoInterface::class); + $exportInfo->setSkipAttr([]); + $exportInfo->setFileFormat('csv'); + $exportInfo->setEntity('catalog_product'); + $exportInfo->setLocale('de_DE'); + $exportInfo->setExportFilter($this->objectManager->get(Json::class)->serialize($exportFilter)); + $this->assertStringContainsString('Katalog, Suche', $exportManager->export($exportInfo)); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php new file mode 100644 index 0000000000000..7a214fc733054 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\ProductTest; + +use Magento\Catalog\Model\Indexer\Product\Price\Processor as ProductPriceIndexer; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\CatalogImportExport\Model\Import\ProductTestBase; +use Magento\Framework\App\Area; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\TestFramework\Fixture\AppArea; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; + +#[ + AppArea(Area::AREA_ADMINHTML), + DbIsolation(false), +] +class ProductIndexersInvalidationTest extends ProductTestBase +{ + #[ + DataFixture('Magento/Catalog/_files/multiple_products.php'), + ] + public function testIndexersState() : void + { + $indexerRegistry = BootstrapHelper::getObjectManager()->get(IndexerRegistry::class); + $fulltextIndexer = $indexerRegistry->get(FulltextIndexer::INDEXER_ID); + $priceIndexer = $indexerRegistry->get(ProductPriceIndexer::INDEXER_ID); + $fulltextIndexer->reindexAll(); + $priceIndexer->reindexAll(); + + $this->assertFalse($fulltextIndexer->isScheduled()); + $this->assertFalse($priceIndexer->isScheduled()); + $this->assertFalse($fulltextIndexer->isInvalid()); + $this->assertFalse($priceIndexer->isInvalid()); + + $this->importFile('products_to_import.csv'); + + $this->assertFalse($fulltextIndexer->isInvalid()); + $this->assertFalse($priceIndexer->isInvalid()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php index 6d32f64e075c6..30bddb9751746 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php @@ -7,8 +7,10 @@ namespace Magento\CatalogImportExport\Model\Import\ProductTest; +use Magento\Catalog\Helper\Data as CatalogConfig; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\CatalogImportExport\Model\Import\ProductTestBase; use Magento\CatalogInventory\Model\StockRegistry; use Magento\Framework\Api\SearchCriteria; @@ -16,7 +18,15 @@ use Magento\Framework\Filesystem; use Magento\ImportExport\Helper\Data; use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Test\Fixture\CsvFile as CsvFileFixture; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Translation\Test\Fixture\Translation; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; /** @@ -725,4 +735,90 @@ public function testImportProductWithTaxClassNone(): void $simpleProduct = $this->getProductBySku('simple2'); $this->assertSame('0', (string) $simpleProduct->getTaxClassId()); } + + #[ + Config(CatalogConfig::XML_PATH_PRICE_SCOPE, CatalogConfig::PRICE_SCOPE_WEBSITE, ScopeInterface::SCOPE_STORE), + DataFixture(ProductFixture::class, ['price' => 10], 'product'), + DataFixture( + CsvFileFixture::class, + [ + 'rows' => [ + ['sku', 'store_view_code', 'price'], + ['$product.sku$', 'default', '9'], + ['$product.sku$', 'default', '8'], + ] + ], + 'file' + ), + ] + public function testImportPriceInStoreViewShouldNotOverrideDefaultScopePrice(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $sku = $fixtures->get('product')->getSku(); + $pathToFile = $fixtures->get('file')->getAbsolutePath(); + $importModel = $this->createImportModel($pathToFile); + $this->assertErrorsCount(0, $importModel->validateData()); + $importModel->importData(); + $product = $this->productRepository->get($sku, storeId: Store::DEFAULT_STORE_ID, forceReload: true); + $this->assertEquals(10, $product->getPrice()); + $product = $this->productRepository->get($sku, storeId: Store::DISTRO_STORE_ID, forceReload: true); + $this->assertEquals(9, $product->getPrice()); + } + + #[ + DataFixture( + Translation::class, + [ + 'string' => 'Not Visible Individually', + 'translate' => 'Nicht individuell sichtbar', + 'locale' => 'de_DE', + ] + ), + DataFixture(ProductFixture::class, as: 'p1'), + DataFixture( + CsvFileFixture::class, + [ + 'rows' => [ + ['sku', 'visibility'], + ['$p1.sku$', 'Nicht individuell sichtbar'], + ] + ], + 'file' + ) + ] + public function testImportWithSpecificLocale(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $p1 = $fixtures->get('p1'); + $pathToFile = $fixtures->get('file')->getAbsolutePath(); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + + $importModel = $this->objectManager->create( + \Magento\ImportExport\Model\Import::class + ); + $importModel->setData( + [ + 'entity' => 'catalog_product', + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + Import::FIELD_NAME_VALIDATION_STRATEGY => + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR, + Import::FIELD_NAME_ALLOWED_ERROR_COUNT => 0, + Import::FIELD_FIELD_SEPARATOR => ',', + 'locale' => 'de_DE' + ] + ); + $importModel->validateSource($source); + $this->assertErrorsCount(0, $importModel->getErrorAggregator()); + $importModel->importSource(); + $simpleProduct = $this->getProductBySku($p1->getSku()); + $this->assertEquals(Product\Visibility::VISIBILITY_NOT_VISIBLE, (int) $simpleProduct->getVisibility()); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php index a6f1448d61311..043475c99cc36 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php @@ -154,15 +154,19 @@ public function testImportWithBackordersDisabled(): void * * @magentoDataFixture mediaImportImageFixture * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled */ public function testProductStockStatusShouldBeUpdated() { + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); $this->importFile('disable_product.csv'); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('enable_product.csv'); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); } @@ -171,22 +175,25 @@ public function testProductStockStatusShouldBeUpdated() * Test that product stock status is updated after import on schedule * * @magentoDataFixture mediaImportImageFixture - * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/CatalogImportExport/_files/cataloginventory_stock_item_update_by_schedule.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDbIsolation disabled */ public function testProductStockStatusShouldBeUpdatedOnSchedule() { - /** * @var $indexProcessor \Magento\Indexer\Model\Processor */ $indexProcessor = $this->objectManager->create(\Magento\Indexer\Model\Processor::class); + $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('disable_product.csv'); $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('enable_product.csv'); $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php index e174cb33733ae..3fef30f96e36e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php @@ -43,7 +43,7 @@ protected function setUp(): void * Test saving of stock item on product save by 'setStockData' method (deprecated) via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveBySetStockData() { @@ -60,7 +60,7 @@ public function testSaveBySetStockData() * via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveBySetData() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php index 7593d0e8b46df..55d1d09051c59 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php @@ -65,7 +65,7 @@ protected function setUp(): void * Test saving of stock item by product data via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSave() { @@ -83,7 +83,7 @@ public function testSave() * product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveManuallyCreatedStockItem() { @@ -104,7 +104,7 @@ public function testSaveManuallyCreatedStockItem() * product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveManuallyUpdatedStockItem() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php new file mode 100644 index 0000000000000..e7a02f2531fcc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Indexer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\AppIsolation; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; +use Magento\TestFramework\Helper\Bootstrap; + +class IndexerBuilderInScheduledModeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var RuleProductProcessor + */ + private $ruleProductProcessor; + + /** + * @var CollectionFactory + */ + private $productCollectionFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp(): void + { + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->ruleProductProcessor = Bootstrap::getObjectManager()->get(RuleProductProcessor::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + #[ + DbIsolation(false), + AppIsolation(true), + DataFixture('Magento/Catalog/_files/product_with_options.php'), + DataFixture('Magento/CatalogRule/_files/catalog_rule_10_off_not_logged.php'), + ] + public function testReindexOfDependentIndexer(): void + { + $indexer = $this->ruleProductProcessor->getIndexer(); + $indexer->reindexAll(); + $indexer->setScheduled(true); + + $product = $this->productRepository->get('simple'); + $productId = (int)$product->getId(); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(9, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $product->setPrice(100); + $this->productRepository->save($product); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(9, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $indexer->reindexList([$productId]); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(90, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $indexer->setScheduled(false); + } + + /** + * Get the product from the product collection + * + * @param int $productId + * @return DataObject + */ + private function getProductFromCollection(int $productId) : DataObject + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addIdFilter($productId); + $productCollection->addPriceData(); + $productCollection->load(); + return $productCollection->getFirstItem(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index c032f47d88348..8f4b804de1577 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -165,6 +165,42 @@ public function testExecuteWithArrayInParam(array $searchParams): void ); } + /** + * Advanced search test by difference product attributes. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * @dataProvider testDataForAttributesCombination + * + * @param array $searchParams + * @param bool $isProductShown + * @return void + */ + public function testExecuteForAttributesCombination(array $searchParams, bool $isProductShown): void + { + $this->getRequest()->setQuery( + $this->_objectManager->create( + Parameters::class, + [ + 'values' => $searchParams + ] + ) + ); + $this->dispatch('catalogsearch/advanced/result'); + $responseBody = $this->getResponse()->getBody(); + + if ($isProductShown) { + $this->assertStringContainsString('Simple product name', $responseBody); + } else { + $this->assertStringContainsString( + 'We can't find any items matching these search criteria.', + $responseBody + ); + } + $this->assertStringNotContainsString('Not visible simple product', $responseBody); + } + /** * Data provider with array in params values * @@ -339,4 +375,71 @@ private function getAttributeOptionValueByOptionLabel(string $attributeCode, str return $attribute->getSource()->getOptionId($optionLabel); } + + /** + * Data provider with strings for quick search. + * + * @return array + */ + public function testDataForAttributesCombination(): array + { + return [ + 'search_product_by_name_and_price' => [ + [ + 'name' => 'Simple product name', + 'sku' => '', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 99, + 'to' => 101, + ], + 'test_searchable_attribute' => '', + ], + true + ], + 'search_product_by_name_and_price_not_shown' => [ + [ + 'name' => 'Simple product name', + 'sku' => '', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 101, + 'to' => 102, + ], + 'test_searchable_attribute' => '', + ], + false + ], + 'search_product_by_sku' => [ + [ + 'name' => '', + 'sku' => 'simple_for_search', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 99, + 'to' => 101, + ], + 'test_searchable_attribute' => '', + ], + true + ], + 'search_product_by_sku_not_shown' => [ + [ + 'name' => '', + 'sku' => 'simple_for_search', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 990, + 'to' => 1010, + ], + 'test_searchable_attribute' => '', + ], + false + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.php new file mode 100644 index 0000000000000..f80097ddf3be2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.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\Api\ProductRepositoryInterface; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; + +/** + * Class for product url rewrites tests + * + */ +class ProductUrlRewriteVisibilityTest extends AbstractUrlRewriteTest +{ + private const URL_KEY_EMPTY_MESSAGE = 'Failed asserting URL key is empty for the given product'; + + /** @var string */ + private $suffix; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->suffix = $this->config->getValue( + ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @dataProvider invisibleProductDataProvider + * @param array $expectedData + * @return void + */ + #[ + DataFixture(ProductFixture::class, ['sku' => 'simple', 'name' => 'Simple Url Test Product', + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE]), + ] + public function testUrlRewriteOnInvisibleProductEdit(array $expectedData): void + { + $product = $this->productRepository->get('simple', true, 0, true); + $this->assertUrlKeyEmpty($product, self::URL_KEY_EMPTY_MESSAGE); + + //Update visibility and check the database entry + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $product = $this->productRepository->save($product); + + $productUrlRewriteCollection = $this->getEntityRewriteCollection($product->getId()); + $this->assertRewrites( + $productUrlRewriteCollection, + $this->prepareData($expectedData, (int)$product->getId()) + ); + + //Update visibility and check if the entry is removed from the database + $product = $this->productRepository->get('simple', true, 0, true); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $product = $this->productRepository->save($product); + + $this->assertUrlKeyEmpty($product, self::URL_KEY_EMPTY_MESSAGE); + } + + /** + * @return array + */ + public function invisibleProductDataProvider(): array + { + return [ + [ + 'expected_data' => [ + [ + 'request_path' => 'simple-url-test-product%suffix%', + 'target_path' => 'catalog/product/view/id/%id%', + ], + ], + ], + ]; + } + + /** + * Assert URL key is empty in database for the given product + * + * @param $product + * @param string $message + * + * @return void + */ + public function assertUrlKeyEmpty($product, $message = ''): void + { + $productUrlRewriteItems = $this->getEntityRewriteCollection($product->getId())->getItems(); + $this->assertEmpty($productUrlRewriteItems, $message); + } + + /** + * @inheritdoc + */ + protected function getUrlSuffix(): string + { + return $this->suffix; + } + + /** + * @inheritdoc + */ + protected function getEntityType(): string + { + return DataProductUrlRewriteDatabaseMap::ENTITY_TYPE; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php index f6d5021b4bcdb..0a4711911774d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php @@ -77,11 +77,15 @@ function (UrlRewrite $urlRewrite) { $expected = [ 'store-1-key.html', // the Default store - 'cat-1/store-1-key.html', // the Default store with Category URL key - '/store-1-key.html', // an anchor URL the Default store + 'cat-1/store-1-key.html', // the Default store with Category URL key, first store view + '/store-1-key.html', // an anchor URL the Default store, first store view + 'cat-1/store-1-key.html', // the Default store with Category URL key, second store view + '/store-1-key.html', // an anchor URL the Default store, second store view 'p002.html', // the Secondary store - 'cat-1-2/p002.html', // the Secondary store with Category URL key - '/p002.html', // an anchor URL the Secondary store + 'cat-1-2/p002.html', // the Secondary store with Category URL key, first store view + '/p002.html', // an anchor URL the Secondary store, first store view + 'cat-1-2/p002.html', // the Secondary store with Category URL key, second store view + '/p002.html', // an anchor URL the Secondary store, second store view ]; self::assertEquals($expected, $actual, 'Generated URLs rewrites do not match.'); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php index 5f5f9dda20c66..3d6babde36246 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php @@ -18,7 +18,7 @@ class RouterTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->markTestIncomplete('MAGETWO-3393'); + $this->markTestSkipped('MAGETWO-3393'); $this->_model = new \Magento\Cms\Controller\Router( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\App\ActionFactory::class @@ -45,7 +45,7 @@ protected function setUp(): void */ public function testMatch() { - $this->markTestIncomplete('MAGETWO-3393'); + $this->markTestSkipped('MAGETWO-3393'); $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\App\RequestInterface::class); //Open Node diff --git a/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php b/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php index b65b2942b6f7b..ef41ac6b76bb1 100644 --- a/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php @@ -30,6 +30,11 @@ protected function setUp(): void $this->system = $this->objectManager->create(System::class); } + public static function tearDownAfterClass(): void + { + unset($_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ']); + } + public function testGetValueDefaultScope() { $this->assertEquals( @@ -56,8 +61,8 @@ public function testGetValueDefaultScope() */ public function testEnvGetValueStoreScope() { - $this->system->clean(); $_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ'] = 'test_env_value'; + $this->system->clean(); $this->assertEquals( 'value1.db.default.test', diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php new file mode 100644 index 0000000000000..a6222ac17f97d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ConfigurableProduct\Test\Fixture\Attribute as AttributeFixture; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Eav\Model\Config; +use Magento\Catalog\Api\Data\ProductAttributeInterface; + +/** + * Checks creating attribute options process. + * + * @see \Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute\UpdateProductAttributeTest + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UpdateProductAttributeTest extends AbstractBackendController +{ + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $productRepository->cleanCache(); + $this->productAttributeRepository = $this->_objectManager->create(ProductAttributeRepositoryInterface::class); + $this->eavConfig = $this->_objectManager->create(Config::class); + } + + /** + * Test updating a product attribute and checking the frontend_class for the sku attribute. + * + * @return void + * @throws LocalizedException + */ + #[ + DataFixture(AttributeFixture::class, as: 'attr'), + ] + public function testAttributeWithBackendTypeHasSameValueInFrontendClass() + { + // Load the 'sku' attribute. + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->productAttributeRepository->get('sku'); + $expectedFrontEndClass = $attribute->getFrontendClass(); + + // Save the attribute. + $this->productAttributeRepository->save($attribute); + + // Check that the value of the frontend_class changed or not. + try { + $skuAttribute = $this->eavConfig->getAttribute('catalog_product', 'sku'); + $this->assertEquals($expectedFrontEndClass, $skuAttribute->getFrontendClass()); + } catch (LocalizedException $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 4b6fac496df0d..438ba1ed75e87 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -656,7 +656,7 @@ protected function getUsedProducts() */ public function testAddCustomOptionToConfigurableChildProduct(): void { - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Required custom options cannot be added to a simple product that is a part of a composite product.' ); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php index beab52c142402..958f6da93e071 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php @@ -109,6 +109,20 @@ public function testGenerateSimpleProductsWithPartialData(array $productsData): } } + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @dataProvider generateSimpleProductsWithPartialDataDataProvider + * @param array $productsData + * @return void + */ + public function testGeneratedSimpleProductInheritTaxClassFromParent(array $productsData): void + { + $this->product->setTaxClassId(2); + $generatedProduct = $this->variationHandler->generateSimpleProducts($this->product, $productsData); + $product = $this->productRepository->getById(reset($generatedProduct)); + $this->assertEquals(2, $product->getTaxClassId()); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php index 02c2e78689386..c1cbe3de85c17 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Phrase; +use Magento\Framework\Session\Generic; use Magento\Framework\Url\EncoderInterface; use Magento\TestFramework\TestCase\AbstractController; @@ -33,6 +34,11 @@ class LoginPostTest extends AbstractController */ private $customerUrl; + /** + * @var Generic + */ + private $generic; + /** * @inheritdoc */ @@ -43,6 +49,7 @@ protected function setUp(): void $this->session = $this->_objectManager->get(Session::class); $this->urlEncoder = $this->_objectManager->get(EncoderInterface::class); $this->customerUrl = $this->_objectManager->get(Url::class); + $this->generic = $this->_objectManager->get(Generic::class); } /** @@ -220,6 +227,25 @@ public function testNoFormKeyLoginPostAction(): void ); } + /** + * @magentoConfigFixture current_store customer/startup/redirect_dashboard 1 + * @magentoConfigFixture current_store customer/captcha/enable 0 + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testVisitorForCustomerLoginPostAction(): void + { + $this->assertEmpty($this->generic->getVisitorData()); + $this->prepareRequest('customer@example.com', 'password'); + $this->dispatch('customer/account/loginPost'); + $this->assertTrue($this->session->isLoggedIn()); + $this->assertRedirect($this->stringContains('customer/account/')); + $this->assertNotEmpty($this->generic->getVisitorData()['visitor_id']); + $this->assertNotEmpty($this->generic->getVisitorData()['customer_id']); + } + /** * Prepare request * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php index b4581bf8d5da6..401a74e752522 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php @@ -9,7 +9,13 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; @@ -35,6 +41,12 @@ class ForgotPasswordTest extends TestCase private $newPasswordLinkPath = "//a[contains(@href, 'customer/account/createPassword') " . "and contains(text(), 'Set a New Password')]"; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var DataFixtureStorage */ + private $fixtures; + /** * @inheritdoc */ @@ -45,6 +57,8 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); } /** @@ -61,4 +75,45 @@ public function testForgotPassword(): void $this->assertTrue($result); $this->assertEquals(1, Xpath::getElementsCountForXpath($this->newPasswordLinkPath, $messageContent)); } + + /** + * @return void + * @throws LocalizedException + */ + #[ + DataFixture(Customer::class, ['email' => 'customer@search.example.com'], as: 'customer'), + ] + public function testResetPasswordFlowStorefront(): void + { + // Forgot password section; + $customer = $this->fixtures->get('customer'); + $email = $customer->getEmail(); + $customerId = (int)$customer->getId(); + $result = $this->accountManagement->initiatePasswordReset($email, AccountManagement::EMAIL_RESET); + $message = $this->transportBuilder->getSentMessage(); + $messageContent = $message->getBody()->getParts()[0]->getRawContent(); + $this->assertTrue($result); + $this->assertEquals(1, Xpath::getElementsCountForXpath($this->newPasswordLinkPath, $messageContent)); + + // Send reset password link + $defaultWebsiteId = (int)$this->storeManager->getWebsite('base')->getId(); + $this->accountManagement->initiatePasswordReset($email, AccountManagement::EMAIL_RESET, $defaultWebsiteId); + + // login with old credentials + $this->assertEquals( + $customerId, + (int)$this->accountManagement->authenticate($email, 'password')->getId() + ); + + // Change password + $this->accountManagement->changePassword($email, 'password', 'new_Password123'); + + // Login with new credentials + $this->accountManagement->authenticate($email, 'new_Password123'); + + $this->assertEquals( + $customerId, + $this->accountManagement->authenticate($email, 'new_Password123')->getId() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index 86c0290edc78a..c9196a581f542 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; @@ -16,6 +17,7 @@ use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Url as UrlBuilder; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -331,7 +333,7 @@ public function testValidateResetPasswordLinkTokenExpired() public function testValidateResetPasswordLinkTokenInvalid() { $resetToken = 'lsdj579slkj5987slkj595lkj'; - $invalidToken = 0; + $invalidToken = '0'; $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); try { $this->accountManagement->validateResetPasswordLinkToken(1, $invalidToken); @@ -481,7 +483,7 @@ public function testResetPasswordTokenExpired() public function testResetPasswordTokenInvalid() { $resetToken = 'lsdj579slkj5987slkj595lkj'; - $invalidToken = 0; + $invalidToken = '0'; $password = 'new_Password123'; $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); @@ -604,7 +606,18 @@ public function testResendConfirmationNotNeeded() */ public function testIsEmailAvailable() { - $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); + + if (!$guestLoginConfig) { + $this->assertTrue($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + } else { + $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + } } /** @@ -612,7 +625,18 @@ public function testIsEmailAvailable() */ public function testIsEmailAvailableNoWebsiteSpecified() { - $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com')); + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); + + if (!$guestLoginConfig) { + $this->assertTrue($this->accountManagement->isEmailAvailable('customer@example.com')); + } else { + $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com')); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php index efba3be6e78cf..5216507384575 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php @@ -7,6 +7,8 @@ use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; @@ -32,5 +34,15 @@ } catch (NoSuchEntityException $exception) { //Already deleted } +/** Remove customer group */ +/** @var GroupRepositoryInterface $groupRepository */ +$groupRepository = $objectManager->create(GroupRepositoryInterface::class); +/** @var SearchCriteriaBuilder $searchBuilder */ +$searchBuilder = $objectManager->create(SearchCriteriaBuilder::class); +foreach ($groupRepository->getList($searchBuilder->create())->getItems() as $group) { + if ('custom_group_2' === $group->getCode()) { + $groupRepository->delete($group); + } +} $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php new file mode 100644 index 0000000000000..58676013f5b19 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Data\Address; +use Magento\Customer\Model\Data\Customer; +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; +use Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelDehydrator; +use Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelHydrator; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class CustomerModelHydratorDehydratorTest extends TestCase +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var ExtractCustomerData + */ + private $resolverDataExtractor; + + /** + * @var SerializerInterface + */ + private $serializer; + + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->resolverDataExtractor = $this->objectManager->get(ExtractCustomerData::class); + $this->serializer = $this->objectManager->get(SerializerInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + */ + public function testModelHydration(): void + { + $customerModel = $this->customerRepository->get('customer_with_addresses@test.com'); + $resolverData = $this->resolverDataExtractor->execute($customerModel); + /** @var ModelDehydrator $dehydrator */ + $dehydrator = $this->objectManager->get(ModelDehydrator::class); + $dehydrator->dehydrate($resolverData); + + $serializedData = $this->serializer->serialize($resolverData); + $resolverData = $this->serializer->unserialize($serializedData); + + /** @var ModelHydrator $hydrator */ + $hydrator = $this->objectManager->get(ModelHydrator::class); + $hydrator->hydrate($resolverData); + $this->assertInstanceOf(Customer::class, $resolverData['model']); + $assertionMap = [ + 'model_id' => 'id', + 'firstname' => 'firstname', + 'lastname' => 'lastname' + ]; + + foreach ($assertionMap as $resolverDataField => $modelDataField) { + $this->assertEquals( + $resolverData[$resolverDataField], + $resolverData['model']->{'get' . $this->camelize($modelDataField)}() + ); + } + + $this->assertEquals( + $customerModel->getExtensionAttributes(), + $resolverData['model']->getExtensionAttributes() + ); + + $assertionMap = [ + 'id' => 'id', + 'customer_id' => 'customer_id', + 'region_id' => 'region_id', + 'country_id' => 'country_id', + 'street' => 'street', + 'postcode' => 'postcode', + 'city' => 'city', + 'firstname' => 'firstname', + 'lastname' => 'lastname', + ]; + + $addresses = $resolverData['model']->getAddresses(); + foreach ($addresses as $key => $address) { + $this->assertInstanceOf(Address::class, $address); + foreach ($assertionMap as $resolverDataField => $modelDataField) { + $this->assertEquals( + $resolverData['addresses'][$key][$resolverDataField], + $address->{'get' . $this->camelize($modelDataField)}() + ); + } + } + } + + /** + * Transform snake case to camel case + * + * @param $string + * @param $separator + * @return string + */ + private function camelize($string, $separator = '_') + { + return str_replace($separator, '', ucwords($string, $separator)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index 7a7fcfc558d07..41414c2b5f9f6 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -280,13 +280,19 @@ public function testRequestToShip( 'store', null ); + $convmap = [0x80, 0x10FFFF, 0, 0x1FFFFF]; + $content = mb_encode_numericentity( + file_get_contents(__DIR__ . '/../_files/response_shipping_label.xml'), + $convmap, + 'UTF-8' + ); //phpcs:disable Magento2.Functions.DiscouragedFunction $this->httpClient->nextResponses( [ new Response( 200, [], - utf8_encode(file_get_contents(__DIR__ . '/../_files/response_shipping_label.xml')) + $content ) ] ); @@ -310,6 +316,9 @@ public function testRequestToShip( 'items' => [ 'item1' => [ 'name' => $productName, + 'qty' => 1, + 'weight' => '0.454000000001', + 'price' => '10.00', ], ], ], @@ -416,8 +425,13 @@ private function getExpectedLabelRequestXml( $expectedRequestElement->Shipper->CountryName = $countryNames[$origCountryId]; $expectedRequestElement->RegionCode = $regionCode; + if ($origCountryId !== $destCountryId) { + $expectedRequestElement->ExportDeclaration->ExportLineItem->ManufactureCountryCode = $origCountryId; + } + if ($isProductNameContainsSpecialChars) { $expectedRequestElement->ShipmentDetails->Pieces->Piece->PieceContents = self::PRODUCT_NAME_SPECIAL_CHARS; + $expectedRequestElement->ExportDeclaration->ExportLineItem->Description = self::PRODUCT_NAME_SPECIAL_CHARS; } return $expectedRequestElement->asXML(); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml index 8cdeaa6018119..9a0d6a4fd46db 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml @@ -48,6 +48,26 @@ <DeclaredCurrency>USD</DeclaredCurrency> <TermsOfTrade>DAP</TermsOfTrade> </Dutiable> + <ExportDeclaration> + <InvoiceNumber/> + <InvoiceDate>1970-01-01</InvoiceDate> + <ExportLineItem> + <LineNumber>item1</LineNumber> + <Quantity>1</Quantity> + <QuantityUnit>PCS</QuantityUnit> + <Description>item_name</Description> + <Value>10.00</Value> + <Weight> + <Weight>0.454000000001</Weight> + <WeightUnit>K</WeightUnit> + </Weight> + <GrossWeight> + <Weight>0.454000000001</Weight> + <WeightUnit>K</WeightUnit> + </GrossWeight> + <ManufactureCountryCode>GB</ManufactureCountryCode> + </ExportLineItem> + </ExportDeclaration> <Reference xmlns=""> <ReferenceID>shipment reference</ReferenceID> <ReferenceType>St</ReferenceType> diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php index 3f4fc72e4258f..a5728a58a5309 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php @@ -43,6 +43,10 @@ protected function setUp(): void */ public function testCorrectElasticsearchClientEs7() { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ + $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); + } + $connection = $this->connectionManager->getConnection(); $this->assertInstanceOf(Elasticsearch::class, $connection); } diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php new file mode 100644 index 0000000000000..fdf7776d7a84c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php @@ -0,0 +1,154 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Email\Model\Template; + +use Magento\Email\Model\ResourceModel\Template\Collection as TemplateCollection; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\Phrase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\Bootstrap as TestFrameworkBootstrap; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class NewAccountEmailTemplateTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var array + */ + protected $storeData = []; + + /** + * Set up + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->storeData['name'] = $this->config->getValue( + 'general/store_information/name', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['phone'] = $this->config->getValue( + 'general/store_information/phone', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['city'] = $this->config->getValue( + 'general/store_information/city', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['country'] = $this->config->getValue( + 'general/store_information/country_id', + ScopeInterface::SCOPE_STORES + ); + } + + /** + * @magentoConfigFixture current_store general/store_information/name TestStore + * @magentoConfigFixture default_store general/store_information/phone 5124666492 + * @magentoConfigFixture default_store general/store_information/hours 10 to 2 + * @magentoConfigFixture default_store general/store_information/street_line1 1 Test Dr + * @magentoConfigFixture default_store general/store_information/street_line2 2nd Addr Line + * @magentoConfigFixture default_store general/store_information/city Austin + * @magentoConfigFixture default_store general/store_information/zip 78739 + * @magentoConfigFixture default_store general/store_information/country_id US + * @magentoConfigFixture default_store general/store_information/region_id 57 + * @magentoDataFixture Magento/Email/Model/_files/email_template.php + */ + public function testNewAccountEmailTemplate(): void + { + + /** @var MutableScopeConfigInterface $config */ + $config = Bootstrap::getObjectManager() + ->get(MutableScopeConfigInterface::class); + $config->setValue( + 'admin/emails/email_template', + $this->getCustomEmailTemplateId( + 'template_fixture' + ) + ); + + /** @var \Magento\User\Model\User $userModel */ + $userModel = Bootstrap::getObjectManager()->get(\Magento\User\Model\User::class); + $userModel->setFirstname( + 'John' + )->setLastname( + 'Doe' + )->setUsername( + 'user1' + )->setPassword( + TestFrameworkBootstrap::ADMIN_PASSWORD + )->setEmail( + 'user1@magento.com' + ); + $userModel->save(); + + $userModel->sendNotificationEmailsIfRequired(); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = Bootstrap::getObjectManager() + ->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + $sentMessage->getBodyText(); + + $storeText = implode(',', $this->storeData); + + $this->assertStringContainsString("John,", $sentMessage->getBodyText()); + $this->assertStringContainsString("TestStore", $storeText); + $this->assertStringContainsString("5124666492", $storeText); + $this->assertStringContainsString("Austin", $storeText); + $this->assertStringContainsString("US", $storeText); + } + + /** + * Return email template id by origin template code + * + * @param string $origTemplateCode + * @return int|null + * @throws NotFoundException + */ + private function getCustomEmailTemplateId(string $origTemplateCode): ?int + { + $templateId = null; + $templateCollection = Bootstrap::getObjectManager() + ->create(TemplateCollection::class); + foreach ($templateCollection as $template) { + if ($template->getOrigTemplateCode() == $origTemplateCode) { + $templateId = (int) $template->getId(); + } + } + if ($templateId === null) { + throw new NotFoundException(new Phrase( + 'Customized %templateCode% email template not found', + ['templateCode' => $origTemplateCode] + )); + } + + return $templateId; + } +} 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 6d5f760d7894d..88a104e6e29e1 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 @@ -12,7 +12,8 @@ [ '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_type' => \Magento\Email\Model\Template::TYPE_TEXT, + 'orig_template_code' => 'template_fixture' ] ); $template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html new file mode 100644 index 0000000000000..4f3075decc275 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html @@ -0,0 +1,48 @@ + + +<p class="greeting">{{trans "%first_name," first_name=$user.firstname}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p> + {{trans + 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' + + customer_url=$this.getUrl($store,'customer/account/',[_nosid:1]) + |raw}} +</p> +<ul> + <li><strong>{{trans "Email:"}}</strong> {{var customer.email}}</li> + <li><strong>{{trans "Password:"}}</strong> <em>{{trans "Password you set when creating account"}}</em></li> +</ul> +<p> + {{trans + 'Forgot your account password? Click <a href="%reset_url">here</a> to reset it.' + + reset_url="$this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])" + |raw}} +</p> +<p>{{trans "When you sign in to your account, you will be able to:"}}</p> +<ul> + <li>{{trans "Proceed through checkout faster"}}</li> + <li>{{trans "Check the status of orders"}}</li> + <li>{{trans "View past orders"}}</li> + <li>{{trans "Store alternative addresses (for shipping to multiple family members and friends)"}}</li> +</ul> + +<ul> + <li><strong>{{trans "Base Unsecure URL:"}}</strong> {{config path="web/unsecure/base_url"}}</li> + <li><strong>{{trans "Base Secure URL:"}}</strong> {{config path="web/secure/base_url"}}</li> + <li><strong>{{trans "General Contact Name:"}}</strong>{{config path="trans_email/ident_general/name"}} </li> + <li><strong>{{trans "General Contact Email:"}}</strong>{{config path="trans_email/ident_general/email"}} </li> + <li><strong>{{trans "Sales Representative Contact Name:"}}</strong>{{config path="trans_email/ident_sales/name"}} </li> + <li><strong>{{trans "Sales Representative Contact Email:"}}</strong>{{config path="trans_email/ident_sales/email"}} </li> + <li><strong>{{trans "Store Name:"}}</strong>{{config path="general/store_information/name"}} </li> + <li><strong>{{trans "Store Phone Number:"}}</strong> {{config path="general/store_information/phone"}}</li> + <li><strong>{{trans "Store Hours:"}}</strong> {{config path="general/store_information/hours"}}</li> + <li><strong>{{trans "Country:"}}</strong> {{config path="general/store_information/country_id"}}</li> + <li><strong>{{trans "Region/State:"}}</strong>{{config path="general/store_information/region_id"}} </li> + <li><strong>{{trans "Zip/Postal Code:"}}</strong>{{config path="general/store_information/postcode"}} </li> + <li><strong>{{trans "City:"}}</strong> {{config path="general/store_information/city"}}</li> + <li><strong>{{trans "Street Address 1:"}}</strong> {{config path="general/store_information/street_line1"}}</li> + <li><strong>{{trans "Street Address 2:"}}</strong>{{config path="general/store_information/street_line2"}}</li> +</ul> + diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php index 7bd4b3a99d1bf..470e434542ecb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php @@ -34,6 +34,9 @@ public function testGenerateFileFromString() $contentType = 'application/pdf'; $fileContent = ['type' => 'string', 'value' => '']; $response = $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + ob_start(); + $response->sendResponse(); + ob_end_clean(); /** @var ContentType $contentTypeHeader */ $contentTypeHeader = $response->getHeader('Content-type'); @@ -48,7 +51,10 @@ public function testGenerateFileFromString() /* Check the file is removed after generation if the corresponding option is set */ $fileContent = ['type' => 'string', 'value' => '', 'rm' => true]; - $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + $response = $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + ob_start(); + $response->sendResponse(); + ob_end_clean(); self::assertFalse($varDirectory->isFile($filename)); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php index 93ed3d84c8452..72ba459997c32 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php @@ -36,7 +36,8 @@ public function testCreate() ]; $connection = $this->model->create($dbConfig); $this->assertInstanceOf(\Magento\Framework\DB\Adapter\AdapterInterface::class, $connection); - $this->assertClassHasAttribute('logger', get_class($connection)); + $this->assertIsObject($connection); + $this->assertTrue(property_exists($connection, 'logger')); $object = new ReflectionClass(get_class($connection)); $attribute = $object->getProperty('logger'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php index f25880e10c811..9d48de03c736e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php @@ -7,11 +7,12 @@ namespace Magento\Framework\Backup; use Magento\Backup\Helper\Data; +use Magento\Backup\Model\ResourceModel\Db; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Module\Setup; use Magento\TestFramework\Helper\Bootstrap; -use PHPUnit\Framework\TestCase; +use Magento\Framework\Backup\BackupInterface; /** * Provide tests for \Magento\Framework\Backup\Db. @@ -32,16 +33,17 @@ public static function setUpBeforeClass(): void } /** - * Test db backup includes triggers. + * Test db backup and rollback including triggers. * * @magentoConfigFixture default/system/backup/functionality_enabled 1 * @magentoDataFixture Magento/Framework/Backup/_files/trigger.php * @magentoDbIsolation disabled */ - public function testBackupIncludesCustomTriggers() + public function testBackupAndRollbackIncludesCustomTriggers() { $helper = Bootstrap::getObjectManager()->get(Data::class); $time = time(); + /** BackupInterface $backupManager */ $backupManager = Bootstrap::getObjectManager()->get(Factory::class)->create( Factory::TYPE_DB )->setBackupExtension( @@ -60,6 +62,12 @@ public function testBackupIncludesCustomTriggers() '/CREATE TRIGGER `?test_custom_trigger`? AFTER INSERT ON `?'. $tableName . '`? FOR EACH ROW/', $content ); + + // Test rollback + $backupResourceModel = Bootstrap::getObjectManager()->get(Db::class); + $backupManager->setResourceModel($backupResourceModel); + $backupManager->rollback(); + //Clean up. $write->delete('/backups/' . $time . '_db_testbackup.sql'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php new file mode 100644 index 0000000000000..c113b1e2f9b64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Cache; + +use Magento\Framework\Lock\Backend\Database; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +class LockGuardedCacheLoaderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $om; + + /** + * @var LockGuardedCacheLoader|null + */ + private ?LockGuardedCacheLoader $lockGuardedCacheLoader; + + /** + * @param string|null $name + * @param array $data + * @param $dataName + */ + public function __construct(?string $name = null, array $data = [], $dataName = '') + { + $this->om = Bootstrap::getObjectManager(); + + parent::__construct($name, $data, $dataName); + } + + protected function setUp(): void + { + $this->lockGuardedCacheLoader = $this->om + ->create( + LockGuardedCacheLoader::class, + [ + 'locker' => $this->om->get(Database::class) + ] + ); + } + + /** + * @dataProvider dataProviderLockGuardedCacheLoader + * + * @param $lockName + * @param $dataLoader + * @param $dataCollector + * @param $dataSaver + * @param $expected + * @return void + */ + public function testLockedLoadData( + $lockName, + $dataLoader, + $dataCollector, + $dataSaver, + $expected + ) { + $result = $this->lockGuardedCacheLoader->lockedLoadData( + $lockName, + $dataLoader, + $dataCollector, + $dataSaver + ); + + $this->assertEquals($expected, $result); + } + + /** + * @return array[] + */ + public function dataProviderLockGuardedCacheLoader(): array + { + return [ + 'Data loader read' => [ + 'lockName', + function () { + return ['data1', 'data2']; + }, + function () { + return ['data3', 'data4']; + }, + function () { + return new \stdClass(); + }, + ['data1', 'data2'], + ], + 'Data collector read' => [ + 'lockName', + function () { + return false; + }, + function () { + return ['data3', 'data4']; + }, + function () { + return new \stdClass(); + }, + ['data3', 'data4'], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample index 42f766c786c0b..ab8588f229bb4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample @@ -72,7 +72,17 @@ class Proxy extends \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespa */ public function __clone() { - $this->_subject = clone $this->_getSubject(); + if ($this->_subject) { + $this->_subject = clone $this->_getSubject(); + } + } + + /** + * Debug proxied instance + */ + public function __debugInfo() + { + return ['i' => $this->_subject]; } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php index 1655dca029c1e..91f47b1f49391 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php @@ -1,7 +1,5 @@ <?php /** - * Test for \Magento\Framework\Filesystem\Driver\File - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -13,6 +11,7 @@ use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Framework\Filesystem\Driver\File * Verify File class */ class FileTest extends TestCase @@ -104,7 +103,7 @@ public function testReadDirectoryRecursively(): void 'foo/bar/file_two.txt', 'foo/file_three.txt', ]; - $expected = array_map(['self', 'getTestPath'], $paths); + $expected = array_map([self::class, 'getTestPath'], $paths); $actual = $this->driver->readDirectoryRecursively($this->getTestPath('foo')); sort($actual); $this->assertEquals($expected, $actual); @@ -174,6 +173,18 @@ public function testFilePutWithoutContents(): void $this->assertEquals(0, $this->driver->filePutContents($path, '')); } + /** + * Delete a not existing file + * + * @return void + * @throws FileSystemException + */ + public function testDeleteFileEdge(): void + { + $path = $this->absolutePath . 'foo/file_four.txt'; + $this->assertEquals(true, $this->driver->deleteFile($path)); + } + /** * Remove generated directories. * diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 96e31a753adaa..ec41e3b159e7f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -194,11 +194,7 @@ enumValues(includeDeprecated: true) { $response = $this->graphQlController->dispatch($request); $output = $this->jsonSerializer->unserialize($response->getContent()); $expectedOutput = require __DIR__ . '/../_files/schema_response_sdl_description.php'; - $schemaResponseFields = $output['data']['__schema']['types']; - $schemaResponseFieldsFirstHalf = array_slice($schemaResponseFields, 0, 25); - $schemaResponseFieldsSecondHalf = array_slice($schemaResponseFields, -21, 21); - $mergedSchemaResponseFields = array_merge($schemaResponseFieldsFirstHalf, $schemaResponseFieldsSecondHalf); foreach ($expectedOutput as $searchTerm) { $sortFields = ['inputFields', 'fields']; @@ -215,7 +211,7 @@ function ($a, $b) { } $this->assertTrue( - (in_array($searchTerm, $mergedSchemaResponseFields)), + (in_array($searchTerm, $schemaResponseFields)), 'Missing type in the response' ); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php index 9b5ea2f361ba9..03de1a5ac09ab 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php @@ -12,25 +12,27 @@ class ObjectManagerTest extends \PHPUnit\Framework\TestCase /**#@+ * Test class with type error */ - const TEST_CLASS_WITH_TYPE_ERROR = \Magento\Framework\ObjectManager\TestAsset\ConstructorWithTypeError::class; + public const TEST_CLASS_WITH_TYPE_ERROR = + \Magento\Framework\ObjectManager\TestAsset\ConstructorWithTypeError::class; /**#@+ * Test classes for basic instantiation */ - const TEST_CLASS = \Magento\Framework\ObjectManager\TestAsset\Basic::class; + public const TEST_CLASS = \Magento\Framework\ObjectManager\TestAsset\Basic::class; - const TEST_CLASS_INJECTION = \Magento\Framework\ObjectManager\TestAsset\BasicInjection::class; + public const TEST_CLASS_INJECTION = \Magento\Framework\ObjectManager\TestAsset\BasicInjection::class; /**#@-*/ /**#@+ * Test classes and interface to test preferences */ - const TEST_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\TestAssetInterface::class; + public const TEST_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\TestAssetInterface::class; - const TEST_INTERFACE_IMPLEMENTATION = \Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation::class; + public const TEST_INTERFACE_IMPLEMENTATION = + \Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation::class; - const TEST_CLASS_WITH_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\InterfaceInjection::class; + public const TEST_CLASS_WITH_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\InterfaceInjection::class; /**#@-*/ @@ -141,7 +143,8 @@ public function testNewInstance($actualClassName, array $properties = [], $expec $object = new ReflectionClass($actualClassName); if ($properties) { foreach ($properties as $propertyName => $propertyClass) { - $this->assertClassHasAttribute($propertyName, $actualClassName); + $this->assertIsObject($testObject); + $this->assertTrue(property_exists($testObject, $propertyName)); $attribute = $object->getProperty($propertyName); $attribute->setAccessible(true); $propertyObject = $attribute->getValue($testObject); diff --git a/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php b/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php index 4a6c542a110bf..df400561e2ffa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php @@ -1,7 +1,5 @@ <?php /** - * Test case for \Magento\Framework\Profiler - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -25,7 +23,8 @@ public function testApplyConfigWithDrivers(array $config, array $expectedDrivers { $profiler = new \Magento\Framework\Profiler(); $profiler::applyConfig($config, ''); - $this->assertClassHasAttribute('_drivers', \Magento\Framework\Profiler::class); + $this->assertIsObject($profiler); + $this->assertTrue(property_exists($profiler, '_drivers')); $object = new ReflectionClass(\Magento\Framework\Profiler::class); $attribute = $object->getProperty('_drivers'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php b/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php index 98e2777467e89..d296661227b16 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php @@ -96,7 +96,7 @@ public function testProcessResponseBody($originalText, $expectedText) { $actualText = $originalText; $this->_model->processResponseBody($actualText, false); - $this->markTestIncomplete('Bug MAGE-2494'); + $this->markTestSkipped('Bug MAGE-2494'); $expected = new \DOMDocument(); $expected->preserveWhiteSpace = false; diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index ad4491b166cfe..859a912bc4f2f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -205,7 +205,7 @@ public function testSetGetRouteName() $this->model->setRouteName('catalog'); $this->assertEquals('catalog', $this->model->getRouteName()); - $this->markTestIncomplete('setRouteName() logic is unclear.'); + $this->markTestSkipped('setRouteName() logic is unclear.'); } public function testSetGetControllerName() @@ -213,7 +213,7 @@ public function testSetGetControllerName() $this->model->setControllerName('product'); $this->assertEquals('product', $this->model->getControllerName()); - $this->markTestIncomplete('setControllerName() logic is unclear.'); + $this->markTestSkipped('setControllerName() logic is unclear.'); } public function testSetGetActionName() @@ -221,7 +221,7 @@ public function testSetGetActionName() $this->model->setActionName('view'); $this->assertEquals('view', $this->model->getActionName()); - $this->markTestIncomplete('setActionName() logic is unclear.'); + $this->markTestSkipped('setActionName() logic is unclear.'); } /** diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php new file mode 100644 index 0000000000000..c63e8cdc8c015 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php @@ -0,0 +1,341 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\App; + +use Magento\Framework\App\Http as HttpApp; +use Magento\Framework\App\Request\HttpFactory as RequestFactory; +use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\App\State\Comparator; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Tests the dispatch method in the GraphQl Controller class using a simple product query + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoAppArea graphql + * @magentoDataFixture Magento/Catalog/_files/multiple_mixed_products.php + * @magentoDataFixture Magento/Catalog/_files/categories.php + * + */ +class GraphQlStateTest extends \PHPUnit\Framework\TestCase +{ + private const CONTENT_TYPE = 'application/json'; + + /** @var ObjectManagerInterface */ + private ObjectManagerInterface $objectManager; + + /** @var Comparator */ + private Comparator $comparator; + + /** @var RequestFactory */ + private RequestFactory $requestFactory; + + /** + * @return void + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->comparator = $this->objectManager->create(Comparator::class); + $this->requestFactory = $this->objectManager->get(RequestFactory::class); + parent::setUp(); + } + + /** + * Runs various GraphQL queries and checks if state of shared objects in Object Manager have changed + * + * @dataProvider queryDataProvider + * @param string $query + * @param array $variables + * @param string $operationName + * @param string $expected + * @return void + * @throws \Exception + */ + public function testState(string $query, array $variables, string $operationName, string $expected): void + { + $jsonEncodedRequest = json_encode([ + 'query' => $query, + 'variables' => $variables, + 'operationName' => $operationName + ]); + $output1 = $this->request($jsonEncodedRequest, $operationName, true); + $this->assertStringContainsString($expected, $output1); + $output2 = $this->request($jsonEncodedRequest, $operationName); + $this->assertStringContainsString($expected, $output2); + $this->assertEquals($output1, $output2); + } + + /** + * @param string $query + * @param string $operationName + * @param bool $firstRequest + * @return string + * @throws \Exception + */ + private function request(string $query, string $operationName, bool $firstRequest = false): string + { + $this->comparator->rememberObjectsStateBefore($firstRequest); + $response = $this->doRequest($query); + $this->comparator->rememberObjectsStateAfter($firstRequest); + $result = $this->comparator->compare($operationName); + $this->assertEmpty( + $result, + sprintf( + '%d objects changed state during request. Details: %s', + count($result), + var_export($result, true) + ) + ); + return $response; + } + + /** + * Process the GraphQL request + * + * @param string $query + * @return string + */ + private function doRequest(string $query) + { + $request = $this->requestFactory->create(); + $request->setContent($query); + $request->setMethod('POST'); + $request->setPathInfo('/graphql'); + $request->getHeaders()->addHeaders(['content_type' => self::CONTENT_TYPE]); + $unusedResponse = $this->objectManager->create(HttpResponse::class); + $httpApp = $this->objectManager->create( + HttpApp::class, + ['request' => $request, 'response' => $unusedResponse] + ); + $actualResponse = $httpApp->launch(); + return $actualResponse->getContent(); + } + + /** + * Queries, variables, operation names, and expected responses for test + * + * @return array[] + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function queryDataProvider(): array + { + return [ + 'Get Navigation Menu by category_id' => [ + <<<'QUERY' + query navigationMenu($id: Int!) { + category(id: $id) { + id + name + product_count + path + children { + id + name + position + level + url_key + url_path + product_count + children_count + path + productImagePreview: products(pageSize: 1) { + items { + small_image { + label + url + } + } + } + } + } + } + QUERY, + ['id' => 4], + 'navigationMenu', + '"id":4,"name":"Category 1.1","product_count":2,' + ], + 'Get Product Search by product_name' => [ + <<<'QUERY' + query productDetailByName($name: String, $onServer: Boolean!) { + products(filter: { name: { match: $name } }) { + items { + id + sku + name + ... on ConfigurableProduct { + configurable_options { + attribute_code + attribute_id + id + label + values { + default_label + label + store_label + use_default_value + value_index + } + } + variants { + product { + #fashion_color + #fashion_size + id + media_gallery_entries { + disabled + file + label + position + } + sku + stock_status + } + } + } + meta_title @include(if: $onServer) + meta_keyword @include(if: $onServer) + meta_description @include(if: $onServer) + } + } + } + QUERY, + ['name' => 'Configurable%20Product', 'onServer' => false], + 'productDetailByName', + '"sku":"configurable","name":"Configurable Product"' + ], + 'Get List of Products by category_id' => [ + <<<'QUERY' + query category($id: Int!, $currentPage: Int, $pageSize: Int) { + category(id: $id) { + product_count + description + url_key + name + id + breadcrumbs { + category_name + category_url_key + __typename + } + products(pageSize: $pageSize, currentPage: $currentPage) { + total_count + items { + id + name + # small_image + # short_description + url_key + special_price + special_from_date + special_to_date + price { + regularPrice { + amount { + value + currency + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + } + QUERY, + ['id' => 4, 'currentPage' => 1, 'pageSize' => 12], + 'category', + '"url_key":"category-1-1","name":"Category 1.1"' + ], + 'Get Simple Product Details by name' => [ + <<<'QUERY' + query productDetail($name: String, $onServer: Boolean!) { + productDetail: products(filter: { name: { match: $name } }) { + items { + sku + name + price { + regularPrice { + amount { + currency + value + } + } + } + description {html} + media_gallery_entries { + label + position + disabled + file + } + ... on ConfigurableProduct { + configurable_options { + attribute_code + attribute_id + id + label + values { + default_label + label + store_label + use_default_value + value_index + } + } + variants { + product { + id + media_gallery_entries { + disabled + file + label + position + } + sku + stock_status + } + } + } + meta_title @include(if: $onServer) + # Yes, Products have `meta_keyword` and + # everything else has `meta_keywords`. + meta_keyword @include(if: $onServer) + meta_description @include(if: $onServer) + } + } + } + QUERY, + ['name' => 'Simple Product1', 'onServer' => false], + 'productDetail', + '"sku":"simple1","name":"Simple Product1"' + ], + 'Get Url Info by url_key' => [ + <<<'QUERY' + query resolveUrl($urlKey: String!) { + urlResolver(url: $urlKey) { + type + id + } + } + QUERY, + ['urlKey' => 'no-route'], + 'resolveUrl', + '"type":"CMS_PAGE","id":1' + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php new file mode 100644 index 0000000000000..0b3afd71b2abb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\App\State; + +use Magento\Framework\ObjectManagerInterface; + +/** + * Collects shared objects from ObjectManager and clones properties for later comparison + */ +class Collector +{ + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Recursively clone objects in array. + * + * @param array $array + * @return array + */ + private function cloneArray(array $array) : array + { + return array_map( + function ($element) { + if (is_object($element)) { + return clone $element; + } + if (is_array($element)) { + return $this->cloneArray($element); + } + return $element; + }, + $array + ); + } + + /** + * Gets shared objects from ObjectManager using reflection and clones properties that are objects + * + * @return array + * @throws \Exception + */ + public function getSharedObjects(): array + { + $sharedObjects = []; + $obj = new \ReflectionObject($this->objectManager); + if (!$obj->hasProperty('_sharedInstances')) { + throw new \Exception('Cannot get shared objects from ' . get_class($this->objectManager)); + } + do { + $property = $obj->getProperty('_sharedInstances'); + $property->setAccessible(true); + $didClone = false; + foreach ($property->getValue($this->objectManager) as $serviceName => $object) { + if (array_key_exists($serviceName, $sharedObjects)) { + continue; + } + if ($object instanceof \Magento\Framework\ObjectManagerInterface) { + continue; + } + $objReflection = new \ReflectionObject($object); + $properties = []; + foreach ($objReflection->getProperties() as $property) { + $propName = $property->getName(); + $property->setAccessible(true); + $value = $property->getValue($object); + if (is_object($value)) { + $didClone = true; + $properties[$propName] = clone $value; + continue; + } elseif (is_array($value)) { + $didClone = true; + $properties[$propName] = $this->cloneArray($value); + } else { + $properties[$propName] = $value; + } + } + $sharedObjects[$serviceName] = [$object, $properties]; + } + // Note: We have to check again because sometimes cloning objects can indirectly cause adding to Object Manager + } while ($didClone); + return $sharedObjects; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php new file mode 100644 index 0000000000000..6e0cdcc373996 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\App\State; + +/** + * Compare object state between requests + */ +class Comparator +{ + /** + * @var Collector + */ + private Collector $collector; + + /** @var array */ + private array $objectsStateBefore = []; + + /** + * @var array + */ + private array $objectsStateAfter = []; + + /** + * @var array|null + */ + private ?array $skipList = null; + + /** + * @var array|null + */ + private ?array $filterList = null; + + /** + * @param Collector $collector + */ + public function __construct(Collector $collector) + { + $this->collector = $collector; + } + + /** + * Remember shared object state before request + * + * @param bool $firstRequest + * @throws \Exception + */ + public function rememberObjectsStateBefore(bool $firstRequest): void + { + if ($firstRequest) { + $this->objectsStateBefore = $this->collector->getSharedObjects(); + } + } + + /** + * Remember shared object state after request + * + * @param bool $firstRequest + * @throws \Exception + */ + public function rememberObjectsStateAfter(bool $firstRequest): void + { + $this->objectsStateAfter = $this->collector->getSharedObjects(); + if ($firstRequest) { + // on the end of first request add objects to init object state pool + $this->objectsStateBefore = array_merge($this->objectsStateAfter, $this->objectsStateBefore); + } + } + + /** + * Compare objectsStateAfter with objectsStateBefore + * + * @param string $operationName + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function compare(string $operationName): array + { + $compareResults = []; + $skipList = $this->getSkipList($operationName); + $filterList = $this->getFilterList(); + $filterListParentClasses = $filterList['parents'] ?? []; + $filterListServices = $filterList['services'] ?? []; + $filterListAll = $filterList['all'] ?? []; + foreach ($this->objectsStateAfter as $serviceName => $service) { + [$object, $properties] = $service; + if (array_key_exists($serviceName, $skipList)) { + continue; + } + $objectState = []; + if (!isset($this->objectsStateBefore[$serviceName])) { + $compareResults[$serviceName] = 'new object appeared after first request'; + } else { + $propertiesToFilterList = []; + if (isset($filterListServices[$serviceName])) { + $propertiesToFilterList[] = $filterListServices[$serviceName]; + } + foreach ($filterListParentClasses as $parentClass => $excludeProperties) { + if ($object instanceof $parentClass) { + $propertiesToFilterList[] = $excludeProperties; + } + } + if ($filterListAll) { + $propertiesToFilterList[] = $filterListAll; + } + $properties = $this->filterProperties($properties, $propertiesToFilterList); + [$beforeObject, $beforeProperties] = $this->objectsStateBefore[$serviceName]; + if ($beforeObject !== $object) { + $compareResults[$serviceName] = 'has new instance of object'; + } + foreach ($properties as $propertyName => $propertyValue) { + $result = $this->checkValues($beforeProperties[$propertyName] ?? null, $propertyValue); + if ($result) { + $objectState[$propertyName] = $result; + } + } + } + if ($objectState) { + $compareResults[$serviceName] = $objectState; + } + } + return $compareResults; + } + + /** + * Filters properties by the list of property filters + * + * @param array $properties + * @param array $propertiesToFilterList + * @return array + */ + private function filterProperties($properties, $propertiesToFilterList): array + { + return array_diff_key($properties, ...$propertiesToFilterList); + } + + /** + * Gets skipList, loading it if needed + * + * @param string $operationName + * @return array + */ + private function getSkipList($operationName): array + { + if ($this->skipList === null) { + $skipListList = []; + foreach (glob(__DIR__ . '/../../_files/state-skip-list*.php') as $skipListFile) { + $skipListList[] = include($skipListFile); + } + $this->skipList = array_merge_recursive(...$skipListList); + } + return array_merge($this->skipList['*'], $this->skipList[$operationName] ?? []); + } + + /** + * Gets filterList, loading it if needed + * + * @return array + */ + private function getFilterList(): array + { + if ($this->filterList === null) { + $filterListList = []; + foreach (glob(__DIR__ . '/../../_files/state-filter-list*.php') as $filterListFile) { + $filterListList[] = include($filterListFile); + } + $this->filterList = array_merge_recursive(...$filterListList); + } + return $this->filterList; + } + + /** + * Formats value by type + * + * @param mixed $type + * @return array + */ + private function formatValue($type): array + { + $type = is_array($type) ? $type : [$type]; + $data = []; + foreach ($type as $k => $v) { + if (is_object($v)) { + $v = get_class($v); + } elseif (is_array($v)) { + $v = $this->formatValue($v); + } + $data[$k] = $v; + } + return $data; + } + + /** + * Compares the values, returns the differences. + * + * @param mixed $before + * @param mixed $after + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function checkValues($before, $after): array + { + $result = []; + $typeBefore = gettype($before); + $typeAfter = gettype($after); + if ($typeBefore !== $typeAfter) { + $result['before'] = $this->formatValue($before); + $result['after'] = $this->formatValue($after); + return $result; + } + switch ($typeBefore) { + case 'boolean': + case 'integer': + case 'double': + case 'string': + if ($before !== $after) { + $result['before'] = $before; + $result['after'] = $after; + } + break; + case 'array': + if (count($before) !== count($after) || $before != $after) { + $result['before'] = $this->formatValue($before); + $result['after'] = $this->formatValue($after); + } + break; + case 'object': + if ($before != $after) { + $result['before'] = get_class($before); + $result['after'] = get_class($after); + } + break; + } + return $result; + } +} diff --git a/app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php similarity index 82% rename from app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php index 8570242c65841..beed18868e76f 100644 --- a/app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php @@ -5,11 +5,9 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\Test\Unit\Controller\HttpRequestValidator; +namespace Magento\GraphQl\Controller\HttpRequestValidator; use Magento\Framework\App\HttpRequestInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -19,7 +17,7 @@ class HttpVerbValidatorTest extends TestCase { /** - * @var HttpVerbValidator|MockObject + * @var HttpVerbValidator */ private $httpVerbValidator; @@ -33,7 +31,7 @@ class HttpVerbValidatorTest extends TestCase */ protected function setup(): void { - $objectManager = new ObjectManager($this); + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->requestMock = $this->getMockBuilder(HttpRequestInterface::class) ->disableOriginalConstructor() ->onlyMethods( @@ -47,9 +45,7 @@ protected function setup(): void ) ->getMockForAbstractClass(); - $this->httpVerbValidator = $objectManager->getObject( - HttpVerbValidator::class - ); + $this->httpVerbValidator = $objectManager->get(HttpVerbValidator::class); } /** diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php new file mode 100644 index 0000000000000..2e8eeee6c4cbf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'all' => [ // Note: These will be applied to all services + '_objectManager' => null, + 'objectManager' => null, + '_httpRequest' => null, // TODO ? I think this one is okay + 'pluginList' => null, // Interceptors can change their pluginList?? + '_classReader' => null, + '_eavConfig' => null, + 'eavConfig' => null, + '_eavEntityType' => null, + '_moduleReader' => null, + 'attributeLoader' => null, + 'storeRepository' => null, + 'localeResolver' => null, + '_localeResolver' => null, + ], + 'parents' => [ // Note: these are parent classes and will match their children as well. + Magento\Framework\DataObject::class => ['_underscoreCache' => null], + Magento\Eav\Model\Entity\AbstractEntity::class => [ + '_attributesByTable' => null, + '_attributesByCode' => null, + '_staticAttributes' => null, + ], + Magento\Framework\Model\ResourceModel\Db\AbstractDb::class => ['_tables' => null], + ], + 'services' => [ // Note: These apply only to the service names that match. + Magento\Framework\ObjectManager\ConfigInterface::class => ['_mergedArguments' => null], + Magento\Framework\ObjectManager\DefinitionInterface::class => ['_definitions' => null], + Magento\Framework\App\Cache\Type\FrontendPool::class => ['_instances' => null], + Magento\Framework\GraphQl\Schema\Type\TypeRegistry::class => ['types' => null], + Magento\Framework\Filesystem::class => ['readInstances' => null, 'writeInstances' => null], + Magento\Framework\EntityManager\TypeResolver::class => [ + 'typeMapping' => null + ], + Magento\Framework\App\View\Deployment\Version::class => [ + 'cachedValue' => null // deployment version of static files + ], + Magento\Framework\View\Asset\Minification::class => ['configCache' => null], // TODO: depends on mode + Magento\Eav\Model\Config::class => [ // TODO: is this risky? + 'attributeProto' => null, + 'attributesPerSet' => null, + 'attributes' => null, + '_objects' => null, + '_references' => null, + ], + Magento\Framework\Api\ExtensionAttributesFactory::class => ['classInterfaceMap' => null], + Magento\Catalog\Model\ResourceModel\Category::class => ['_isActiveAttributeId' => null], + Magento\Eav\Model\ResourceModel\Entity\Type::class => ['additionalAttributeTables' => null], + Magento\Framework\Reflection\MethodsMap::class => ['serviceInterfaceMethodsMap' => null], + Magento\Framework\EntityManager\Sequence\SequenceRegistry::class => ['registry' => null], + Magento\Framework\EntityManager\MetadataPool::class => ['registry' => null], + Magento\Framework\App\Config\ScopeCodeResolver::class => ['resolvedScopeCodes' => null], + Magento\Framework\App\ResourceConnection::class => [ + 'config' => null, // $_connectionNames changes + 'connections' => null, + ], + Magento\Framework\Cache\InvalidateLogger::class => ['request' => null], + Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple::class => ['rulePool' => null], + Magento\Framework\View\Template\Html\Minifier::class => ['filesystem' => null], + Magento\Store\Model\Config\Processor\Fallback::class => ['scopes' => null], + 'viewFileFallbackResolver' => ['rulePool' => null], + Magento\Framework\View\Asset\Source::class => ['filesystem' => null], + Magento\Store\Model\StoreResolver::class => ['request' => null], + Magento\Framework\Url\Decoder::class => ['urlBuilder' => null], + Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class => ['request' => null], + Magento\Framework\App\Helper\Context::class => ['_urlBuilder' => null], + Magento\MediaStorage\Helper\File\Storage\Database::class => [ + '_filesystem' => null, + '_request' => null, + '_urlBuilder' => null, + ], + Magento\Framework\Event\Config::class => ['_dataContainer' => null], + Magento\TestFramework\Store\StoreManager::class => ['decoratedStoreManager' => null], + Magento\Eav\Model\ResourceModel\Entity\Attribute::class => ['_eavEntityType' => null], + Magento\Eav\Model\Entity\AttributeLoader::class => ['defaultAttributes' => null, 'config' => null], + Magento\Framework\Validator\Factory::class => ['moduleReader' => null], + Magento\PageCache\Model\Config::class => ['reader' => null], + Magento\Config\Model\Config\Compiler\IncludeElement::class => ['moduleReader' => null], + Magento\Customer\Model\Customer::class => ['_config' => null], + Magento\Framework\Model\Context::class => ['_cacheManager' => null, '_appState' => null], + Magento\Framework\App\Cache\TypeList::class => ['_cache' => null], + Magento\GraphQlCache\Model\CacheId\CacheIdCalculator::class => ['contextFactory' => null], + Magento\Store\Model\Config\Placeholder::class => ['request' => null], + Magento\Framework\Config\Scope::class => ['_areaList' => null], // These were added because we switched to ... + Magento\TestFramework\App\State::class => ['_areaCode' => null], // . + Magento\Framework\Event\Invoker\InvokerDefault::class => ['_appState' => null], // . + Magento\Developer\Model\Logger\Handler\Debug::class => ['state' => null], // . + Magento\Framework\View\Design\FileResolution\Fallback\TemplateFile::class => // . + ['appState' => null], // ... using Magento\Framework\App\Http for the requests + Magento\Store\App\Config\Source\RuntimeConfigSource::class => ['connection' => null], + Magento\Framework\Mview\View\Changelog::class => ['connection' => null], + Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection::class => ['_conn' => null], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php new file mode 100644 index 0000000000000..7033ed2436c6c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/* These classes are skipped completely during comparison. */ +return [ + 'navigationMenu' => [ + Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + Magento\Framework\Session\Generic::class => null, + ], + 'productDetailByName' => [ + Magento\Customer\Model\Session::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Store\Model\GroupRepository::class => null, + ], + 'category' => [ + Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + 'productDetail' => [ + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + 'resolveUrl' => [ + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + '*' => [ + Magento\TestFramework\Interception\PluginList::class => null, + // memory leak, wrong sql, potential issues + Magento\Framework\Event\Config\Data::class => null, + Magento\Framework\App\AreaList::class => null, + 'customRemoteFilesystem' => null, + Magento\Store\App\Config\Type\Scopes::class => null, + Magento\Framework\Module\Dir\Reader::class => null, + Magento\Framework\Module\PackageInfo::class => null, + Magento\Framework\App\Language\Dictionary::class => null, + Magento\Framework\ObjectManager\ConfigInterface::class => null, + Magento\Framework\App\Cache\Type\Config::class => null, + Magento\Framework\Interception\PluginListGenerator::class => null, + Magento\TestFramework\App\Config::class => null, + Magento\TestFramework\Request::class => null, + Magento\Framework\View\FileSystem::class => null, + Magento\Framework\App\Config\FileResolver::class => null, + Magento\TestFramework\ErrorLog\Logger::class => null, + 'translationConfigSourceAggregated' => null, + Magento\Framework\App\Request\Http\Proxy::class => null, + Magento\Framework\Event\Config\Reader\Proxy::class => null, + Magento\Theme\Model\View\Design\Proxy::class => null, + Magento\Translation\Model\Source\InitialTranslationSource\Proxy::class => null, + Magento\Translation\App\Config\Type\Translation::class => null, + Magento\Backend\App\Request\PathInfoProcessor\Proxy::class => null, + Magento\Framework\View\Asset\Source::class => null, + Magento\Framework\Translate\ResourceInterface\Proxy::class => null, + Magento\Framework\Locale\Resolver\Proxy::class => null, + Magento\MediaStorage\Helper\File\Storage\Database::class => null, + Magento\Framework\App\Cache\Proxy::class => null, + Magento\Framework\Translate::class => null, + Magento\Store\Model\StoreManager::class => null, + Magento\Framework\App\Http\Context::class => null, + Magento\TestFramework\Response::class => null, + Magento\Store\Model\WebsiteRepository::class => null, + Magento\Framework\Locale\Resolver::class => null, + Magento\Store\Model\GroupRepository::class => null, + Magento\Store\Model\StoreRepository::class => null, + Magento\Framework\View\Design\Fallback\RulePool::class => null, + Magento\Framework\View\Asset\Repository::class => null, + Magento\Framework\HTTP\Header::class => null, + Magento\Framework\App\Route\Config::class => null, + Magento\Store\Model\System\Store::class => null, + Magento\AwsS3\Driver\CredentialsCache::class => null, + Magento\Eav\Model\Config::class => null, + 'AssetPreProcessorPool' => null, + Magento\GraphQl\Model\Query\ContextFactory::class => null, + 'viewFileMinifiedFallbackResolver' => null, + Magento\Framework\View\Asset\Minification::class => null, + Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class => null, + Magento\Framework\Url::class => null, + Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class => null, + ], + '' => [ + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php new file mode 100644 index 0000000000000..20a032019755b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Plugin\Resolver; + +use Magento\Framework\App\Cache\State as CacheState; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\GraphQlResolverCache\Model\Plugin\Resolver\Cache as CachePlugin; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type; +use Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CacheTest extends TestCase +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @var GraphQlRequest + */ + private $graphQlRequest; + + /** + * @var CacheState + */ + private $cacheState; + + /** + * @var bool + */ + private $origCacheEnabled; + + /** + * @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $loggerMock; + + /** + * @var Type|\PHPUnit\Framework\MockObject\MockObject + */ + private $graphqlResolverCacheMock; + + /** + * @var Type + */ + private $graphQlResolverCache; + + /** + * @var GenericFactorProviderInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $keyFactorMock; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + $this->cacheState = $this->objectManager->get(CacheState::class); + $this->origCacheEnabled = $this->cacheState->isEnabled(Type::TYPE_IDENTIFIER); + if (!$this->origCacheEnabled) { + $this->cacheState->setEnabled(Type::TYPE_IDENTIFIER, true); + $this->cacheState->persist(); + } + $this->graphQlResolverCache = $this->objectManager->get(Type::class); + $this->graphQlResolverCache->clean(); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $this->cacheState->setEnabled(Type::TYPE_IDENTIFIER, $this->origCacheEnabled); + $this->cacheState->persist(); + $this->graphQlResolverCache->clean(); + parent::tearDown(); + } + + /** + * @magentoAppArea graphql + */ + public function testCachingSkippedOnKeyCalculationFailure() + { + $this->preconfigureMocks(); + $this->configurePlugin(); + $this->keyFactorMock->expects($this->any()) + ->method('getFactorValue') + ->willThrowException(new \Exception("Test key factor exception")); + $this->loggerMock->expects($this->once())->method('warning'); + $this->graphqlResolverCacheMock->expects($this->never()) + ->method('load'); + $this->graphqlResolverCacheMock->expects($this->never()) + ->method('save'); + $this->graphQlRequest->send($this->getTestQuery()); + } + + /** + * @magentoAppArea graphql + */ + public function testCachingNotSkippedWhenKeysOk() + { + $this->preconfigureMocks(); + $this->configurePlugin(); + $this->loggerMock->expects($this->never())->method('warning'); + $this->graphqlResolverCacheMock->expects($this->once()) + ->method('load') + ->willReturn(false); + $this->graphqlResolverCacheMock->expects($this->once()) + ->method('save'); + $this->graphQlRequest->send($this->getTestQuery()); + } + + /** + * Configure mocks and object manager for test. + * + * @return void + */ + private function preconfigureMocks() + { + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['warning']) + ->setMockClassName('CacheLoggerMockForTest') + ->getMockForAbstractClass(); + + $this->graphqlResolverCacheMock = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->onlyMethods(['load', 'save']) + ->setMockClassName('GraphqlResolverCacheMockForTest') + ->getMock(); + + $this->keyFactorMock = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorValue', 'getFactorName']) + ->setMockClassName('TestFailingKeyFactor') + ->getMock(); + + $this->objectManager->addSharedInstance($this->keyFactorMock, 'TestFailingKeyFactor'); + + $this->objectManager->configure( + [ + Calculator::class => [ + 'arguments' => [ + 'factorProviders' => [ + 'test_failing' => 'TestFailingKeyFactor' + ] + ] + ] + ] + ); + + $identityProviderMock = $this->getMockBuilder(IdentityInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIdentities']) + ->setMockClassName('TestIdentityProvider') + ->getMock(); + + $identityProviderMock->expects($this->any()) + ->method('getIdentities') + ->willReturn(['test_identity']); + + $this->objectManager->addSharedInstance($identityProviderMock, 'TestIdentityProvider'); + + $this->objectManager->configure( + [ + ResolverIdentityClassProvider::class => [ + 'arguments' => [ + 'cacheableResolverClassNameIdentityMap' => [ + StoreConfigResolver::class => 'TestIdentityProvider' + ] + ] + ] + ] + ); + } + + private function getTestQuery() + { + return <<<QUERY +{ + storeConfig { + id, + code, + store_code, + store_name + } +} +QUERY; + } + + /** + * Reset plugin for the resolver. + * + * @return void + */ + private function configurePlugin() + { + // need to reset plugins list to inject new plugin with mocks as it is cached at runtime + /** @var PluginList $pluginList */ + $pluginList = $this->objectManager->get(PluginList::class); + $pluginList->reset(); + $this->objectManager->removeSharedInstance(CachePlugin::class); + $this->objectManager->addSharedInstance( + $this->objectManager->create(CachePlugin::class, [ + 'logger' => $this->loggerMock, + 'graphQlResolverCache' => $this->graphqlResolverCacheMock + ]), + CachePlugin::class + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php new file mode 100644 index 0000000000000..051aaf3cd2b55 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php @@ -0,0 +1,191 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\KeyCalculator; + +use Magento\CustomerGraphQl\Model\Resolver\Customer; +use Magento\CustomerGraphQl\Model\Resolver\CustomerAddresses; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider; +use Magento\StoreGraphQl\CacheIdFactorProviders\CurrencyProvider; +use Magento\StoreGraphQl\CacheIdFactorProviders\StoreProvider; +use Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for Graphql Resolver-level cache key provider. + */ +class ProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @var Provider + */ + private $provider; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + parent::setUp(); + } + + /** + * Test that generic key provided for non-customized resolver is a generic key provider with default config. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderForGenericKey() + { + $this->provider = $this->objectManager->create(Provider::class); + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $genericCalculator = $this->objectManager->get(Calculator::class); + $calc = $this->provider->getKeyCalculatorForResolver($resolver); + $this->assertSame($genericCalculator, $calc); + } + + /** + * Test that customized provider returns a key calculator that provides factors in certain order. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderNonGenericKey() + { + $this->provider = $this->objectManager->create(Provider::class, [ + 'customFactorProviders' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'store' => 'Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store', + 'currency' => 'Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Currency' + ], + ] + ]); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $storeFactorMock = $this->getMockBuilder(StoreProvider::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $currencyFactorMock = $this->getMockBuilder(CurrencyProvider::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $storeFactorMock->expects($this->any()) + ->method('getFactorName') + ->withAnyParameters() + ->willReturn(StoreProvider::NAME); + $storeFactorMock->expects($this->any()) + ->method('getFactorValue') + ->withAnyParameters() + ->willReturn('default'); + + $currencyFactorMock->expects($this->any()) + ->method('getFactorName') + ->withAnyParameters() + ->willReturn(CurrencyProvider::NAME); + $currencyFactorMock->expects($this->any()) + ->method('getFactorValue') + ->withAnyParameters()->willReturn('USD'); + + $this->objectManager->addSharedInstance($storeFactorMock, StoreProvider::class); + $this->objectManager->addSharedInstance($currencyFactorMock, CurrencyProvider::class); + $expectedKey = hash('sha256', strtoupper(implode('|', ['currency' => 'USD', 'store' => 'default']))); + $calc = $this->provider->getKeyCalculatorForResolver($resolver); + $key = $calc->calculateCacheKey(); + $this->assertNotEmpty($key); + $this->assertEquals($expectedKey, $key); + $this->objectManager->removeSharedInstance(StoreProvider::class); + $this->objectManager->removeSharedInstance(CurrencyProvider::class); + } + + /** + * Test that if different resolvers have same custom key calculator it is not instantiated again. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderSameKeyCalculatorsForDifferentResolvers() + { + $this->provider = $this->objectManager->create( + Provider::class, + [ + 'customFactorProviders' => [ + 'Magento\CustomerGraphQl\Model\Resolver\Customer' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn' + ], + 'Magento\CustomerGraphQl\Model\Resolver\CustomerAddresses' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn' + ] + ] + ] + ); + $customerResolver = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + + $customerAddressResolver = $this->getMockBuilder(CustomerAddresses::class) + ->disableOriginalConstructor() + ->getMock(); + + $calcCustomer = $this->provider->getKeyCalculatorForResolver($customerResolver); + $calcAddress = $this->provider->getKeyCalculatorForResolver($customerAddressResolver); + $this->assertSame($calcCustomer, $calcAddress); + } + + /** + * Test that different key calculators with intersecting factors are not being reused. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderDifferentKeyCalculatorsForDifferentResolvers() + { + $this->provider = $this->objectManager->create(Provider::class, [ + 'customFactorProviders' => [ + 'Magento\CustomerGraphQl\Model\Resolver\Customer' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\Cache\KeyFactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\CacheIdFactorProviders\IsLoggedInProvider' + ], + 'Magento\CustomerGraphQl\Model\Resolver\CustomerAddresses' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\Cache\KeyFactorProvider\CurrentCustomerId', + ] + ] + ]); + $customerResolver = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + + $customerAddressResolver = $this->getMockBuilder(CustomerAddresses::class) + ->disableOriginalConstructor() + ->getMock(); + + $calcCustomer = $this->provider->getKeyCalculatorForResolver($customerResolver); + $calcAddress = $this->provider->getKeyCalculatorForResolver($customerAddressResolver); + $this->assertNotSame($calcCustomer, $calcAddress); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php new file mode 100644 index 0000000000000..ca1186f54d94d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php @@ -0,0 +1,411 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\Cache; + +use Magento\GraphQl\Model\Query\ContextFactoryInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\CalculationException; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\ParentValueFactorProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\ParentValue\ProcessedValueFactorInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Psr\Log\LoggerInterface; + +/** + * Test for graphql resolver-level cache key calculator. + */ +class KeyCalculatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @var ContextFactoryInterface + */ + private $contextFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); + parent::setUp(); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testKeyCalculatorException() + { + $this->expectException(CalculationException::class); + $this->expectExceptionMessage("Test message"); + $exceptionMessage = "Test message"; + + $mock = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willThrowException(new \Exception($exceptionMessage)); + $mock->expects($this->never()) + ->method('getFactorValue') + ->willReturn('value'); + + $this->objectManager->addSharedInstance($mock, 'TestFactorProviderMock'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create( + Calculator::class, + [ + 'factorProviders' => [ + 'test' => 'TestFactorProviderMock' + ] + ] + ); + $keyCalculator->calculateCacheKey(); + } + + /** + * @param array $factorDataArray + * @param array|null $parentResolverData + * @param string|null $expectedCacheKey + * + * @return void + * + * @magentoAppArea graphql + * + * @dataProvider keyFactorDataProvider + */ + public function testKeyCalculator(array $factorDataArray, ?array $parentResolverData, $expectedCacheKey) + { + $this->initMocksForObjectManager($factorDataArray, $parentResolverData); + + $keyFactorProvidersConfig = []; + foreach ($factorDataArray as $factorData) { + $keyFactorProvidersConfig[$factorData['name']] = $this->prepareFactorClassName($factorData); + } + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create( + Calculator::class, + [ + 'factorProviders' => $keyFactorProvidersConfig + ] + ); + $key = $keyCalculator->calculateCacheKey($parentResolverData); + + $this->assertEquals($expectedCacheKey, $key); + + $this->resetMocksForObjectManager($factorDataArray); + } + + /** + * Helper method to initialize object manager with mocks from given test data. + * + * @param array $factorDataArray + * @param array|null $parentResolverData + * @return void + */ + private function initMocksForObjectManager(array $factorDataArray, ?array $parentResolverData) + { + foreach ($factorDataArray as $factor) { + if ($factor['interface'] == GenericFactorProviderInterface::class) { + $mock = $this->getMockBuilder($factor['interface']) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willReturn($factor['name']); + $mock->expects($this->once()) + ->method('getFactorValue') + ->willReturn($factor['value']); + } else { + $mock = $this->getMockBuilder($factor['interface']) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue', 'getFactorValueForParentResolvedData']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willReturn($factor['name']); + $mock->expects($this->never()) + ->method('getFactorValue') + ->willReturn($factor['name']); + $mock->expects($this->once()) + ->method('getFactorValueForParentResolvedData') + ->with($this->contextFactory->get(), $parentResolverData) + ->willReturn($factor['value']); + } + $this->objectManager->addSharedInstance($mock, $this->prepareFactorClassName($factor)); + } + } + + /** + * Get class name from factor data. + * + * @param array $factor + * @return string + */ + private function prepareFactorClassName(array $factor) + { + return $factor['name'] . 'TestFactorMock'; + } + + /** + * Reset all mocks for the object manager by given factor data. + * + * @param array $factorDataArray + * @return void + */ + private function resetMocksForObjectManager(array $factorDataArray) + { + foreach ($factorDataArray as $factor) { + $this->objectManager->removeSharedInstance($this->prepareFactorClassName($factor)); + } + } + + /** + * Test data provider. + * + * @return array[] + */ + public function keyFactorDataProvider() + { + return [ + 'no factors' => [ + 'factorProviders' => [], + 'parentResolverData' => null, + 'expectedCacheKey' => null + ], + 'single factor' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'test', + 'value' => 'testValue' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash('sha256', strtoupper('testValue')), + ], + 'unsorted multiple factors' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'b_testValue' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash('sha256', strtoupper('a_testValue|b_testValue|c_testValue')), + ], + 'unsorted multiple factors with parent data' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'object_123' + ], + ], + 'parentResolverData' => [ + 'object_id' => 123 + ], + 'expectedCacheKey' => hash('sha256', strtoupper('a_testValue|object_123|c_testValue')), + ], + 'unsorted multifactor with no parent data and parent factored interface' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'some value' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash('sha256', strtoupper('a_testValue|some value|c_testValue')), + ], + ]; + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsCalledForParentValueFromCache() + { + $value = [ + 'data' => 'some data', + ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY => 'preprocess me' + ]; + + $this->initFactorMocks(); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->once()) + ->method('preProcessParentValue') + ->with($value); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + 'parent_value' => 'TestValueFactorMock', + 'parent_processed_value' => 'TestProcessedValueFactorMock' + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $this->assertEquals('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $key); + + $this->objectManager->removeSharedInstance('TestValueFactorMock'); + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } + + /** + * @return void + */ + private function initFactorMocks() + { + $mockContextFactor = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMockForAbstractClass(); + + $mockPlainParentValueFactor = $this->getMockBuilder(ParentValueFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue', 'isRequiredOrigData']) + ->getMockForAbstractClass(); + + $mockPlainParentValueFactor->expects($this->any())->method('isRequiredOrigData')->willReturn(false); + + $mockProcessedParentValueFactor = $this->getMockBuilder(ParentValueFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue', 'isRequiredOrigData']) + ->getMockForAbstractClass(); + + $mockProcessedParentValueFactor->expects($this->any())->method('isRequiredOrigData')->willReturn(true); + + $this->objectManager->addSharedInstance($mockPlainParentValueFactor, 'TestValueFactorMock'); + $this->objectManager->addSharedInstance($mockProcessedParentValueFactor, 'TestProcessedValueFactorMock'); + $this->objectManager->addSharedInstance($mockContextFactor, 'TestContextFactorMock'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsNotCalledForParentValueFromResolver() + { + $value = [ + 'data' => 'some data' + ]; + + $this->initFactorMocks(); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->never()) + ->method('preProcessParentValue'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + 'parent_value' => 'TestValueFactorMock', + 'parent_processed_value' => 'TestProcessedValueFactorMock' + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $this->assertEquals('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $key); + + $this->objectManager->removeSharedInstance('TestValueFactorMock'); + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsSkippedForContextOnlyFactors() + { + $mockContextFactor = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMockForAbstractClass(); + + $value = ['data' => 'some data']; + + $this->objectManager->addSharedInstance($mockContextFactor, 'TestContextFactorMock'); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->never()) + ->method('preProcessParentValue'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $this->assertEquals('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $key); + + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php new file mode 100644 index 0000000000000..635c23237d838 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php @@ -0,0 +1,255 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\DataObject; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver; +use Magento\TestFramework\Helper\Bootstrap; + +class HydratorDehydratorProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @var HydratorDehydratorProvider + */ + private $provider; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + $this->getTestProviderConfig() + ); + parent::setUp(); + } + + /** + * @return array + */ + private function getTestProviderConfig() + { + return [ + 'hydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'nested_items_hydrator' => [ + 'sortOrder' => 15, + 'class' => 'TestResolverNestedItemsHydrator' + ], + 'model_hydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelHydrator' + ], + ] + ], + 'dehydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelDehydrator' + ], + ] + ] + ]; + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorChainProvider() + { + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $testResolverData = [ + 'id' => 2, + 'name' => 'test name', + 'model' => new DataObject( + [ + 'some_field' => 'some_data_value', + 'id' => 2, + 'name' => 'test name', + ] + ) + ]; + + $testModelDehydrator = $this->getMockBuilder(DehydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelDehydrator') + ->onlyMethods(['dehydrate']) + ->getMock(); + + $testModelDehydrator->expects($this->once()) + ->method('dehydrate') + ->willReturnCallback(function (&$resolverData) { + $resolverData['model_data'] = $resolverData['model']->getData(); + unset($resolverData['model']); + }); + + $testModelHydrator = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelHydrator') + ->onlyMethods(['hydrate']) + ->getMock(); + $testModelHydrator->expects($this->once()) + ->method('hydrate') + ->willReturnCallback(function (&$resolverData) { + $do = new DataObject($resolverData['model_data']); + $resolverData['model'] = $do; + $resolverData['sortOrderTest_field'] = 'some data'; + }); + $testNestedHydrator = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverNestedItemsHydrator') + ->onlyMethods(['hydrate']) + ->getMock(); + $testNestedHydrator->expects($this->once()) + ->method('hydrate') + ->willReturnCallback(function (&$resolverData) { + $resolverData['model']->setData('nested_data', ['test_nested_data']); + $resolverData['sortOrderTest_field'] = 'other data'; + }); + + $this->objectManager->addSharedInstance($testModelHydrator, 'TestResolverModelHydrator'); + $this->objectManager->addSharedInstance($testNestedHydrator, 'TestResolverNestedItemsHydrator'); + $this->objectManager->addSharedInstance($testModelDehydrator, 'TestResolverModelDehydrator'); + + $dehydrator = $this->provider->getDehydratorForResolver($resolver); + $dehydrator->dehydrate($testResolverData); + + /** @var HydratorInterface $hydrator */ + $hydrator = $this->provider->getHydratorForResolver($resolver); + $hydrator->hydrate($testResolverData); + + // assert that data object is instantiated + $this->assertInstanceOf(DataObject::class, $testResolverData['model']); + // assert object fields + $this->assertEquals(2, $testResolverData['model']->getId()); + $this->assertEquals('test name', $testResolverData['model']->getName()); + // assert mode nested data from second hydrator + $this->assertEquals(['test_nested_data'], $testResolverData['model']->getNestedData()); + $this->assertEquals('some_data_value', $testResolverData['model']->getData('some_field')); + + //verify that hydrators were invoked in designated order + $this->assertEquals('other data', $testResolverData['sortOrderTest_field']); + + // verify that hydrator instance is not recreated + $this->assertSame($hydrator, $this->provider->getHydratorForResolver($resolver)); + + $this->objectManager->removeSharedInstance('TestResolverModelHydrator'); + $this->objectManager->removeSharedInstance('TestResolverNestedItemsHydrator'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorDoesNotExist() + { + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getHydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorClassMismatch() + { + $this->expectExceptionMessage('Hydrator TestResolverModelDehydrator configured for resolver ' + . 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver must implement ' + . 'Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorInterface.'); + $testModelDehydrator = $this->getMockBuilder(DehydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelDehydrator') + ->onlyMethods(['dehydrate']) + ->getMock(); + $this->objectManager->addSharedInstance($testModelDehydrator, 'TestResolverModelDehydrator'); + + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + [ + 'hydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelDehydrator' + ], + ] + ] + ] + ); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getHydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testDehydratorClassMismatch() + { + $this->expectExceptionMessage('Dehydrator TestResolverModelHydrator configured for resolver ' + . 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver must implement ' + . 'Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorInterface.'); + $hydrator = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelHydrator') + ->getMock(); + $this->objectManager->addSharedInstance($hydrator, 'TestResolverModelHydrator'); + + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + [ + 'dehydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelHydrator' + ], + ] + ] + ] + ); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getDehydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testDehydratorDoesNotExist() + { + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getDehydratorForResolver($resolver)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php index 50972ef0325a6..73aa382baafb3 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php @@ -35,7 +35,8 @@ public function testGetEntityAdapterWithValidEntity($entity, $expectedEntityType { $this->_model->setData(['entity' => $entity]); $this->_model->getEntityAttributeCollection(); - $this->assertClassHasAttribute('_entityAdapter', get_class($this->_model)); + $this->assertIsObject($this->_model); + $this->assertTrue(property_exists($this->_model, '_entityAdapter')); $object = new ReflectionClass(get_class($this->_model)); $attribute = $object->getProperty('_entityAdapter'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php b/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php index e561311fc4e7c..85b0b53e64262 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php @@ -77,6 +77,12 @@ public function contentProvider() 2020 ] ], + 'Relevant paths in content without quotes' => [ + 'content {{media url=testDirectory/path.jpg}} content', + [ + 2020 + ] + ], 'Relevant wysiwyg paths in content' => [ 'content <img src="https://domain.com/media/testDirectory/path.jpg"}} content', [ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Driver/QueueTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Driver/QueueTest.php index d1a3115c49872..40cc115dae1f7 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Driver/QueueTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Driver/QueueTest.php @@ -6,6 +6,7 @@ namespace Magento\MysqlMq\Model\Driver; use Magento\MysqlMq\Model\Driver\Queue; +use Magento\MysqlMq\Model\ResourceModel\MessageCollection; /** * Test for MySQL queue driver class. @@ -43,6 +44,11 @@ protected function tearDown(): void /** @var \Magento\Framework\MessageQueue\Config\Data $queueConfig */ $queueConfig = $this->objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); $queueConfig->reset(); + $messageCollection = $this->objectManager->create(MessageCollection::class); + foreach ($messageCollection as $message) { + $message->delete(); + } + parent::tearDown(); } /** @@ -65,4 +71,29 @@ public function testPushAndDequeue() $this->assertArrayHasKey('topic_name', $actualMessageProperties); $this->assertEquals($topicName, $actualMessageProperties['topic_name']); } + + /** + * @magentoDataFixture Magento/MysqlMq/_files/queues.php + */ + public function testCount() + { + /** @var \Magento\Framework\MessageQueue\EnvelopeFactory $envelopFactory */ + $envelopFactory = $this->objectManager->get(\Magento\Framework\MessageQueue\EnvelopeFactory::class); + $messageBody = '{"data": {"body": "Message body"}, "message_id": 1}'; + $topicName = 'some.topic'; + $envelop1 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + $envelop2 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + $envelop3 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + + $this->queue->push($envelop1); + $this->queue->push($envelop2); + $this->queue->push($envelop3); + + // Take first message in progress and reject + $this->queue->reject($this->queue->dequeue()); + // Take second message in progress + $this->queue->dequeue(); + // Assert that only 2 messages are available in queue (message1 and message3) + $this->assertEquals(2, $this->queue->count()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php index 63670e9cb458d..f0c5ce25911f4 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php @@ -8,12 +8,15 @@ namespace Magento\Newsletter\Controller\Subscriber; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\Session; use Magento\Customer\Model\Url; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResource; use Magento\Newsletter\Model\ResourceModel\Subscriber\CollectionFactory; use Magento\Newsletter\Model\ResourceModel\Subscriber\Grid\Collection as GridCollection; +use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\TestCase\AbstractController; use Laminas\Stdlib\Parameters; @@ -222,8 +225,18 @@ public function testWithEmailAssignedToAnotherCustomer(): void $this->session->loginById(1); $this->prepareRequest('customer2@search.example.com'); $this->dispatch('newsletter/subscriber/new'); + $scopeConfig = $this->_objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); - $this->performAsserts('This email address is already assigned to another user.'); + if ($guestLoginConfig) { + $this->performAsserts('This email address is already assigned to another user.'); + } else { + $this->performAsserts('This email address is already subscribed.'); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php index 719d78b07ca3c..34df1deb4ff35 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php @@ -6,8 +6,11 @@ namespace Magento\Newsletter\Model\Plugin; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Security.Superglobal * @magentoAppIsolation enabled */ class PluginTest extends \PHPUnit\Framework\TestCase @@ -24,6 +27,11 @@ class PluginTest extends \PHPUnit\Framework\TestCase */ protected $customerRepository; + /** + * @var TransportBuilderMock + */ + protected $transportBuilderMock; + protected function setUp(): void { $this->accountManagement = Bootstrap::getObjectManager()->get( @@ -32,6 +40,9 @@ protected function setUp(): void $this->customerRepository = Bootstrap::getObjectManager()->get( \Magento\Customer\Api\CustomerRepositoryInterface::class ); + $this->transportBuilderMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class + ); } protected function tearDown(): void @@ -223,4 +234,67 @@ public function testCustomerWithTwoNewsLetterSubscriptions() $extensionAttributes = $customer->getExtensionAttributes(); $this->assertTrue($extensionAttributes->getIsSubscribed()); } + + /** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store newsletter/general/active 1 + * @magentoDataFixture Magento/Customer/_files/customer_welcome_email_template.php + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testCreateAccountWithNewsLetterSubscription(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory */ + $customerFactory = $objectManager->get(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); + $customerDataObject = $customerFactory->create() + ->setFirstname('John') + ->setLastname('Doe') + ->setEmail('customer@example.com'); + $extensionAttributes = $customerDataObject->getExtensionAttributes(); + $extensionAttributes->setIsSubscribed(true); + $customerDataObject->setExtensionAttributes($extensionAttributes); + $this->accountManagement->createAccount($customerDataObject, '123123qW'); + $message = $this->transportBuilderMock->getSentMessage(); + + $this->assertNotNull($message); + $this->assertEquals('Welcome to Main Website Store', $message->getSubject()); + $this->assertStringContainsString( + 'John', + $message->getBody()->getParts()[0]->getRawContent() + ); + $this->assertStringContainsString( + 'customer@example.com', + $message->getBody()->getParts()[0]->getRawContent() + ); + + /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ + $subscriber = $objectManager->create(\Magento\Newsletter\Model\Subscriber::class); + $subscriber->loadByEmail('customer@example.com'); + $this->assertTrue($subscriber->isSubscribed()); + + $this->transportBuilderMock->setTemplateIdentifier( + 'newsletter_subscription_confirm_email_template' + )->setTemplateVars([ + 'subscriber_data' => [ + 'confirmation_link' => $subscriber->getConfirmationLink(), + ], + ])->setTemplateOptions([ + 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID + ]) + ->addTo('customer@example.com') + ->getTransport(); + + $message = $this->transportBuilderMock->getSentMessage(); + + $this->assertNotNull($message); + $this->assertStringContainsString( + $subscriber->getConfirmationLink(), + $message->getBody()->getParts()[0]->getRawContent() + ); + $this->assertEquals('Newsletter subscription confirmation', $message->getSubject()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php index 870d8387be335..3e65ddac46a49 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php @@ -7,24 +7,24 @@ namespace Magento\ProductAlert\Model\Mailing; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; -use Magento\Customer\Api\AccountManagementInterface; -use Magento\Customer\Model\Session; -use Magento\Framework\Locale\Resolver; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Customer\Test\Fixture\Customer as CustomerFixture; use Magento\Framework\Mail\EmailMessage; -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\Framework\View\Design\ThemeInterface; -use Magento\Framework\View\DesignInterface; +use Magento\ProductAlert\Test\Fixture\PriceAlert as PriceAlertFixture; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreRepository; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\Config; use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\ObjectManager; use Magento\Translation\Test\Fixture\Translation as TranslationFixture; @@ -34,7 +34,6 @@ * Test for Product Alert observer * * @magentoAppIsolation enabled - * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AlertProcessorTest extends TestCase @@ -60,9 +59,9 @@ class AlertProcessorTest extends TestCase private $transportBuilder; /** - * @var DesignInterface + * @var DataFixtureStorage */ - private $design; + private $fixtures; /** * @inheritDoc @@ -74,39 +73,73 @@ protected function setUp(): void $this->alertProcessor = $this->objectManager->get(AlertProcessor::class); $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); - $service = $this->objectManager->create(AccountManagementInterface::class); - $customer = $service->authenticate('customer@example.com', 'password'); - $customerSession = $this->objectManager->get(Session::class); - $customerSession->setCustomerDataAsLoggedIn($customer); - $this->design = $this->objectManager->get(DesignInterface::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); } - /** - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 - * @magentoDataFixture Magento/ProductAlert/_files/product_alert.php - */ + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] public function testProcess() { - $this->processAlerts(); + $customerId = (int) $this->fixtures->get('customer')->getId(); + $customerName = $this->fixtures->get('customer')->getName(); + $this->processAlerts($customerId); + $messageContent = $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent(); /** Checking is the email was sent */ $this->assertStringContainsString( - 'John Smith,', - $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent() + $customerName, + $messageContent + ); + $this->assertStringContainsString( + 'Price change alert! We wanted you to know that prices have changed for these products:', + $messageContent ); } - /** - * Check translations for product alerts - * - * @magentoDbIsolation disabled - * @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 - */ #[ + DbIsolation(false), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$', 'code' => 'pt_br_store'], 'store2'), + DataFixture(CustomerFixture::class, ['website_id' => 1], as: 'customer1'), + DataFixture(CustomerFixture::class, ['website_id' => '$website2.id$'], as: 'customer2'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer1.id$', + 'product_id' => '$product.id$', + 'store_id' => 1, + ] + ), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer2.id$', + 'product_id' => '$product.id$', + 'store_id' => '$store2.id$', + ] + ), + DataFixture( + TranslationFixture::class, + [ + 'string' => 'Price change alert! We wanted you to know that prices have changed for these products:', + 'translate' => 'Alerte changement de prix! Nous voulions que vous sachiez' . + ' que les prix ont changé pour ces produits:', + 'locale' => 'fr_FR', + ], + 'frTxt' + ), DataFixture( TranslationFixture::class, [ @@ -114,28 +147,53 @@ public function testProcess() 'translate' => 'Alerta de mudanca de preco! Queriamos que voce soubesse' . ' que os precos mudaram para esses produtos:', 'locale' => 'pt_BR', - ] + ], + 'ptTxt' ), + Config('catalog/productalert/allow_price', 1), + Config('general/locale/code', 'fr_FR', ScopeInterface::SCOPE_STORE, 'default'), + Config('general/locale/code', 'pt_BR', ScopeInterface::SCOPE_STORE, 'pt_br_store'), ] - public function testProcessPortuguese() + public function testEmailShouldBeTranslatedToStoreLanguage() { - // dispatch process() method and check sent message - $this->processAlerts(); + $customer1Id = (int) $this->fixtures->get('customer1')->getId(); + $customer2Id = (int) $this->fixtures->get('customer2')->getId(); + $website2Id = (int) $this->fixtures->get('website2')->getId(); + $frTxt = $this->fixtures->get('frTxt')->getTranslate(); + $ptTxt = $this->fixtures->get('ptTxt')->getTranslate(); + + // Check email from main website + $this->processAlerts($customer1Id); + $message = $this->transportBuilder->getSentMessage(); + $messageContent = $message->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString('/frontend/Magento/luma/fr_FR/', $messageContent); + $this->assertStringContainsString($frTxt, $messageContent); + + // Check email from second website + $this->processAlerts($customer2Id, $website2Id); $message = $this->transportBuilder->getSentMessage(); $messageContent = $message->getBody()->getParts()[0]->getRawContent(); - $expectedText = 'Alerta de mudanca de preco! Queriamos que voce soubesse' . - ' que os precos mudaram para esses produtos:'; $this->assertStringContainsString('/frontend/Magento/luma/pt_BR/', $messageContent); - $this->assertStringContainsString(substr($expectedText, 0, 50), $messageContent); + $this->assertStringContainsString($ptTxt, $messageContent); } - /** - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 - * @magentoDataFixture Magento/ProductAlert/_files/product_alert.php - */ + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] public function testCustomerShouldGetEmailForEveryProductPriceDrop(): void { - $this->processAlerts(); + $customerId = (int) $this->fixtures->get('customer')->getId(); + $productId = (int) $this->fixtures->get('product')->getId(); + $this->processAlerts($customerId); $this->assertStringContainsString( '$10.00', @@ -145,14 +203,13 @@ public function testCustomerShouldGetEmailForEveryProductPriceDrop(): void // Intentional: update product without using ProductRepository // to prevent changes from being cached on application level $product = $this->objectManager->get(ProductFactory::class)->create(); - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $productResource = $this->objectManager->get(ProductResourceModel::class); $product->setStoreId(Store::DEFAULT_STORE_ID); - $productResource->load($product, $productRepository->get('simple')->getId()); + $productResource->load($product, $productId); $product->setPrice(5); $productResource->save($product); - $this->processAlerts(); + $this->processAlerts($customerId); $this->assertStringContainsString( '$5.00', @@ -160,45 +217,45 @@ public function testCustomerShouldGetEmailForEveryProductPriceDrop(): void ); } - /** - * Process price alerts - */ - private function processAlerts(): void - { - $alertType = AlertProcessor::ALERT_TYPE_PRICE; - $customerId = 1; - $websiteId = 1; - - $this->publisher->execute($alertType, [$customerId], $websiteId); - $this->alertProcessor->process($alertType, [$customerId], $websiteId); - } - - /** - * Validate the current theme - * - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 - * @magentoDataFixture Magento/ProductAlert/_files/product_alert.php - */ + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] public function testValidateCurrentTheme() { - $this->design->setDesignTheme( - $this->objectManager->get(ThemeInterface::class) - ); - - $this->processAlerts(); + $customerId = (int) $this->fixtures->get('customer')->getId(); + $this->processAlerts($customerId); $message = $this->transportBuilder->getSentMessage(); $messageContent = $this->getMessageRawContent($message); - $emailDom = new \DOMDocument(); - $emailDom->loadHTML($messageContent); - - $emailXpath = new \DOMXPath($emailDom); - $greeting = $emailXpath->query('//img[@class="photo image"]'); - $this->assertStringContainsString( - 'thumbnail.jpg', - $greeting->item(0)->getAttribute('src') + $img = Xpath::getElementsForXpath('//img[@class="photo image"]', $messageContent); + $this->assertMatchesRegularExpression( + '/frontend\/Magento\/luma\/.+\/thumbnail.jpg$/', + $img->item(0)->getAttribute('src') ); - $this->assertEquals('Magento/luma', $this->design->getDesignTheme()->getCode()); + } + + /** + * @param int $customerId + * @param int $websiteId + * @param string $alertType + * @return void + * @throws \Exception + */ + private function processAlerts( + int $customerId, + int $websiteId = 1, + string $alertType = AlertProcessor::ALERT_TYPE_PRICE + ): void { + $this->alertProcessor->process($alertType, [$customerId], $websiteId); } /** diff --git a/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php b/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php new file mode 100644 index 0000000000000..1b285306e0974 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Monolog\Test\TestCase; + +class DbSchemaTest extends TestCase +{ + /** + * @param string $tableName + * @param string $indexName + * @param array $columns + * @param string $indexType + * @return void + * @dataProvider indexDataProvider + */ + public function testIndex( + string $tableName, + string $indexName, + array $columns, + string $indexType = AdapterInterface::INDEX_TYPE_INDEX, + ): void { + $connection = ObjectManager::getInstance()->get(ResourceConnection::class)->getConnection(); + $indexes = $connection->getIndexList($tableName); + $this->assertArrayHasKey($indexName, $indexes); + $this->assertSame($columns, $indexes[$indexName]['COLUMNS_LIST']); + $this->assertSame($indexType, $indexes[$indexName]['INDEX_TYPE']); + } + + public function indexDataProvider(): array + { + return [ + [ + 'quote', + 'QUOTE_STORE_ID_UPDATED_AT', + ['store_id', 'updated_at'] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php index ab4450cb4e257..8c1904acc39ed 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php @@ -79,73 +79,7 @@ protected function setUp(): void } /** - * @magentoDataFixture Magento/SalesRule/_files/cart_rule_100_percent_off.php - * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @return void - * @throws NoSuchEntityException - */ - public function testRateAppliedToShipping(): void - { - $objectManager = Bootstrap::getObjectManager(); - - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $objectManager->create(CartRepositoryInterface::class); - $customerQuote = $quoteRepository->getForCustomer(1); - $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); - } - - /** - * @magentoConfigFixture current_store carriers/tablerate/active 1 - * @magentoConfigFixture current_store carriers/flatrate/active 0 - * @magentoConfigFixture current_store carriers/freeshipping/active 0 - * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty - * @magentoDataFixture Magento/SalesRule/_files/cart_rule_free_shipping_by_cart.php - * @magentoDataFixture Magento/Sales/_files/quote.php - * @magentoDataFixture Magento/OfflineShipping/_files/tablerates.php - * @return void - */ - public function testTableRateFreeShipping() - { - $objectManager = Bootstrap::getObjectManager(); - /** @var Quote $quote */ - $quote = $objectManager->get(Quote::class); - $quote->load('test01', 'reserved_order_id'); - $cartId = $quote->getId(); - if (!$cartId) { - $this->fail('quote fixture failed'); - } - /** @var QuoteIdMask $quoteIdMask */ - $quoteIdMask = Bootstrap::getObjectManager() - ->create(QuoteIdMaskFactory::class) - ->create(); - $quoteIdMask->load($cartId, 'quote_id'); - //Use masked cart Id - $cartId = $quoteIdMask->getMaskedId(); - $data = [ - 'data' => [ - 'country_id' => "US", - 'postcode' => null, - 'region' => null, - 'region_id' => null - ] - ]; - /** @var EstimateAddressInterface $address */ - $address = $objectManager->create(EstimateAddressInterface::class, $data); - /** @var GuestShippingMethodManagementInterface $shippingEstimation */ - $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); - $result = $shippingEstimation->estimateByAddress($cartId, $address); - $this->assertNotEmpty($result); - $expectedResult = [ - 'method_code' => 'bestway', - 'amount' => 0 - ]; - foreach ($result as $rate) { - $this->assertEquals($expectedResult['amount'], $rate->getAmount()); - $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); - } - } - - /** + * @magentoDbIsolation enabled * @magentoDataFixture Magento/OfflineShipping/_files/tablerates_price.php * @return void * @throws NoSuchEntityException @@ -197,7 +131,7 @@ public function testTableRateWithoutIncludingVirtualProduct() /** * Test table rate amount for the cart that contains some items with free shipping applied. - * + * @magentoDbIsolation enabled * @magentoConfigFixture current_store carriers/tablerate/active 1 * @magentoConfigFixture current_store carriers/flatrate/active 0 * @magentoConfigFixture current_store carriers/freeshipping/active 0 @@ -239,6 +173,73 @@ public function testTableRateWithCartRuleForFreeShipping() $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); $this->assertEquals($expectedResult['amount'], $rate->getAmount()); } + + /** + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_100_percent_off.php + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @return void + * @throws NoSuchEntityException + */ + public function testRateAppliedToShipping(): void + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->create(CartRepositoryInterface::class); + $customerQuote = $quoteRepository->getForCustomer(1); + $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); + } + + /** + * @magentoConfigFixture current_store carriers/tablerate/active 1 + * @magentoConfigFixture current_store carriers/flatrate/active 0 + * @magentoConfigFixture current_store carriers/freeshipping/active 0 + * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_free_shipping_by_cart.php + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/OfflineShipping/_files/tablerates.php + * @return void + */ + public function testTableRateFreeShipping() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Quote $quote */ + $quote = $objectManager->get(Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $data = [ + 'data' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null + ] + ]; + /** @var EstimateAddressInterface $address */ + $address = $objectManager->create(EstimateAddressInterface::class, $data); + /** @var GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); + $result = $shippingEstimation->estimateByAddress($cartId, $address); + $this->assertNotEmpty($result); + $expectedResult = [ + 'method_code' => 'bestway', + 'amount' => 0 + ]; + foreach ($result as $rate) { + $this->assertEquals($expectedResult['amount'], $rate->getAmount()); + $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); + } + } /** * Retrieves quote by reserved order id. diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/config.php b/dev/tests/integration/testsuite/Magento/Review/_files/config.php index 13436974d6305..4e740eef15bb1 100644 --- a/dev/tests/integration/testsuite/Magento/Review/_files/config.php +++ b/dev/tests/integration/testsuite/Magento/Review/_files/config.php @@ -4,12 +4,17 @@ * See COPYING.txt for license details. */ -/** @var Value $config */ use Magento\Framework\App\Config\Value; +use Magento\TestFramework\App\Config as AppConfig; +/** @var Value $config */ $config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); $config->setPath('catalog/review/allow_guest'); $config->setScope('default'); $config->setScopeId(0); $config->setValue(1); $config->save(); + +/** @var AppConfig $appConfig */ +$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class); +$appConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php index ee21150bd6129..dd1b5dbc6dbfb 100644 --- a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php +++ b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php @@ -6,6 +6,7 @@ /** @var Value $config */ use Magento\Framework\App\Config\Value; +use Magento\TestFramework\App\Config as AppConfig; $config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); $config->setPath('catalog/review/allow_guest'); @@ -13,3 +14,7 @@ $config->setScopeId(0); $config->setValue(0); $config->save(); + +/** @var AppConfig $appConfig */ +$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class); +$appConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 27423c67ffe19..9b8bb6a5d3158 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -7,23 +7,37 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Sales\Api\Data\OrderInterfaceFactory; -use Magento\TestFramework\Request; -use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture; +use Magento\Checkout\Test\Fixture\SetBillingAddress; +use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture; +use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture; +use Magento\Checkout\Test\Fixture\SetShippingAddress; use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Test\Fixture\AddProductToCart; +use Magento\Quote\Test\Fixture\CustomerCart; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; -use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Xpath; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * Test for reorder controller. @@ -68,6 +82,16 @@ class ReorderTest extends AbstractBackendController */ private $accountManagement; + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritdoc */ @@ -80,6 +104,8 @@ protected function setUp(): void $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->fixtures = $this->_objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->addressRepository = $this->_objectManager->get(AddressRepositoryInterface::class); } /** @@ -111,10 +137,7 @@ protected function tearDown(): void public function testReorderAfterJSCalendarEnabled(): void { $order = $this->orderFactory->create()->loadByIncrementId('100000001'); - $this->dispatchReorderRequest((int)$order->getId()); - $this->assertRedirect($this->stringContains('backend/sales/order_create')); - $this->quote = $this->getQuote('customer@example.com'); - $this->assertTrue(!empty($this->quote)); + $this->reorder($order, 'customer@example.com'); } /** @@ -208,4 +231,104 @@ private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartIn return array_pop($items); } + + /** + * Verify that the updated customer's addresses have been populated for the quote's billing and shipping addresses + * during reorder. + * + * @return void + * @throws LocalizedException + */ + #[ + DbIsolation(false), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(Customer::class, ['addresses' => [['postcode' => '12345'] ]], as: 'customer'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'quote'), + DataFixture(AddProductToCart::class, ['cart_id' => '$quote.id$', 'product_id' => '$product.id$', 'qty' => 1]), + DataFixture(SetBillingAddress::class, [ + 'cart_id' => '$quote.id$', + 'address' => [ + 'customer_id' => '$customer.id$', + 'save_in_address_book' => 1 + ] + ]), + DataFixture(SetShippingAddress::class, [ + 'cart_id' => '$quote.id$', + 'address' => [ + 'customer_id' => '$customer.id$', + 'save_in_address_book' => 1 + ] + ]), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$quote.id$'], 'order') + ] + public function testReorderBillingAndShippingAddresses(): void + { + $billingPostCode = '98765'; + $shippingPostCode = '01234'; + + $customer = $this->fixtures->get('customer'); + $order = $this->fixtures->get('order'); + + $customerBillingAddressId = $order->getBillingAddress()->getCustomerAddressId(); + $this->updateCustomerAddress((int)$customerBillingAddressId, ['postcode' => $billingPostCode]); + + $customerShippingAddressId = $order->getShippingAddress()->getCustomerAddressId(); + $this->updateCustomerAddress((int)$customerShippingAddressId, ['postcode' => $shippingPostCode]); + + $this->reorder($order, $customer->getEmail()); + + $orderBillingAddress = $order->getBillingAddress(); + $orderShippingAddress = $order->getShippingAddress(); + + $quoteShippingAddress = $this->quote->getShippingAddress(); + $quoteBillingAddress = $this->quote->getBillingAddress(); + + $this->assertEquals($quoteBillingAddress->getStreet(), $orderBillingAddress->getStreet()); + $this->assertEquals($billingPostCode, $quoteBillingAddress->getPostCode()); + $this->assertEquals($quoteBillingAddress->getFirstname(), $orderBillingAddress->getFirstname()); + $this->assertEquals($quoteBillingAddress->getCity(), $orderBillingAddress->getCity()); + $this->assertEquals($quoteBillingAddress->getTelephone(), $orderBillingAddress->getTelephone()); + $this->assertEquals($quoteBillingAddress->getEmail(), $orderBillingAddress->getEmail()); + + $this->assertEquals($quoteShippingAddress->getStreet(), $orderShippingAddress->getStreet()); + $this->assertEquals($shippingPostCode, $quoteShippingAddress->getPostCode()); + $this->assertEquals($quoteShippingAddress->getFirstname(), $orderShippingAddress->getFirstname()); + $this->assertEquals($quoteShippingAddress->getCity(), $orderShippingAddress->getCity()); + $this->assertEquals($quoteShippingAddress->getTelephone(), $orderShippingAddress->getTelephone()); + $this->assertEquals($quoteShippingAddress->getEmail(), $orderShippingAddress->getEmail()); + } + + /** + * Update customer address information + * + * @param int $addressId + * @param array $updateData + * @return void + * @throws LocalizedException + */ + private function updateCustomerAddress(int $addressId, array $updateData): void + { + $address = $this->addressRepository->getById($addressId); + foreach ($updateData as $setFieldName => $setValue) { + $address->setData($setFieldName, $setValue); + } + $this->addressRepository->save($address); + } + + /** + * Place reorder request + * + * @param OrderInterface $order + * @param string $customerEmail + * @return void + */ + private function reorder(OrderInterface $order, string $customerEmail): void + { + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote($customerEmail); + $this->assertNotEmpty($this->quote); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php index 2de06558ab66d..7d76b6cf8524c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php @@ -16,7 +16,7 @@ class CreditmemoTest extends \Magento\TestFramework\TestCase\AbstractBackendCont */ public function testAddCommentAction() { - $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/393'); + $this->markTestSkipped('https://github.com/magento-engcom/msi/issues/393'); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\CatalogInventory\Api\StockIndexInterface $stockIndex */ $stockIndex = $objectManager->get(\Magento\CatalogInventory\Api\StockIndexInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php index 3c93e5598e68a..bf0c090426821 100755 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php @@ -21,6 +21,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\HttpFoundation\Response; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -142,6 +143,7 @@ public function testInitFromOrderAndCreateOrderFromQuoteWithAdditionalOptions() ['additional_option_key' => 'additional_option_value'], $newOrderItem->getProductOptionByCode('additional_options') ); + Response::closeOutputBuffers(1, false); } /** diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php new file mode 100644 index 0000000000000..a840e96809352 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; + +use Magento\Framework\App\Request\Http; +use Magento\Framework\Message\MessageInterface; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test class for \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\Save + * + * @magentoAppArea adminhtml + */ +class SaveTest extends AbstractBackendController +{ + public function testCreateRuleWithOnlyFormkey(): void + { + $requestData = []; + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['You saved the rule.']), + MessageInterface::TYPE_SUCCESS + ); + } + + public function testCreateRuleWithFreeShipping(): void + { + $ruleCollection = Bootstrap::getObjectManager()->create(Collection::class); + $resource = $ruleCollection->getResource(); + $select = $resource->getConnection()->select(); + $select->from($resource->getTable('salesrule'), [new \Zend_Db_Expr('MAX(rule_id)')]); + $maxId = (int)$resource->getConnection()->fetchOne($select); + + $requestData = [ + 'simple_free_shipping' => 1, + ]; + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['You saved the rule.']), + MessageInterface::TYPE_SUCCESS + ); + + $select = $resource->getConnection()->select(); + $select + ->from($resource->getTable('salesrule'), ['simple_free_shipping']) + ->where('rule_id > ?', $maxId); + $simpleFreeShipping = (int)$resource->getConnection()->fetchOne($select); + + $this->assertEquals(1, $simpleFreeShipping); + } + + public function testCreateRuleWithWrongDates(): void + { + $requestData = [ + 'from_date' => '2023-02-02', + 'to_date' => '2023-01-01', + ]; + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['End Date must follow Start Date.']), + MessageInterface::TYPE_ERROR + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php new file mode 100644 index 0000000000000..2a9d1c4b6af93 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php @@ -0,0 +1,67 @@ +<?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\SearchCriteriaBuilder; +use Magento\SalesRule\Api\CouponRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ +class CouponTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CouponRepositoryInterface + */ + private $couponRepository; + + /** + * @var RuleFactory + */ + private $ruleFactory; + + public function setUp(): void + { + parent::setUp(); + /** @var CouponRepositoryInterface couponRepository */ + $this->couponRepository = Bootstrap::getObjectManager()->create(CouponRepositoryInterface::class); + /** @var RuleFactory ruleFactory */ + $this->ruleFactory = Bootstrap::getObjectManager()->create(RuleFactory::class); + } + + /** + * Check that non-autogenerated coupon contains necessary fields received from sales rule + */ + public function testNonAutogeneratedCouponBelongingToRule() + { + $couponCode = '_coupon__code_'; + $rule = $this->ruleFactory->create(); + $rule->setCouponType(2) + ->setUseAutoGeneration(0) + ->setCouponCode($couponCode) + ->setUsesPerCustomer(null) + ->setUsesPerCoupon(null) + ->save(); + + /** @var SearchCriteriaBuilder $criteriaBuilder */ + $criteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); + $couponSearchResult = $this->couponRepository->getList( + $criteriaBuilder->addFilter('code', $couponCode, 'like')->create() + ); + $coupons = $couponSearchResult->getItems(); + $coupon = array_pop($coupons); + + $this->assertEquals(0, $coupon->getUsagePerCustomer()); + $this->assertEquals(0, $coupon->getUsageLimit()); + $this->assertEquals(0, $coupon->getTimesUsed()); + $this->assertEquals(0, $coupon->getType()); + $this->assertNotEmpty($coupon->getCreatedAt()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php index ccab0fc9f1543..1299c5fca09fc 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php @@ -5,22 +5,78 @@ */ namespace Magento\SalesRule\Model\ResourceModel; +use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; +use Magento\SalesRule\Test\Fixture\ProductFoundInCartConditions as ProductFoundInCartConditionsFixture; +use Magento\SalesRule\Test\Fixture\Rule as RuleFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ class RuleTest extends \PHPUnit\Framework\TestCase { + /** + * @var DataFixtureStorage + */ + private $fixtures; + + /** + * @var Rule + */ + private $resource; + + /** + * @inheirtDoc + */ + protected function setUp(): void + { + $this->fixtures = Bootstrap::getObjectManager()->get( + DataFixtureStorageManager::class + )->getStorage(); + $this->resource = Bootstrap::getObjectManager()->create( + Rule::class + ); + } + /** * @magentoDataFixture Magento/SalesRule/_files/rule_custom_product_attribute.php */ public function testAfterSave() { - $resource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule::class - ); - $items = $resource->getActiveAttributes(); + $items = $this->resource->getActiveAttributes(); $this->assertEquals([['attribute_code' => 'attribute_for_sales_rule_1']], $items); } + + #[ + DataFixture( + ProductConditionFixture::class, + ['attribute' => 'category_ids', 'value' => '2'], + 'cond11' + ), + DataFixture( + ProductFoundInCartConditionsFixture::class, + ['conditions' => ['$cond11$']], + 'cond1' + ), + DataFixture( + RuleFixture::class, + ['discount_amount' => 50, 'conditions' => ['$cond1$'], 'is_active' => 0], + 'rule1' + ) + ] + public function testGetActiveAttributes() + { + $rule = $this->fixtures->get('rule1'); + $items = $this->resource->getActiveAttributes(); + $this->assertEquals([], $items); + $rule->setIsActive(1); + $rule->save(); + $items = $this->resource->getActiveAttributes(); + $this->assertEquals([['attribute_code' => 'category_ids']], $items); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php index ff15eb07d4986..ed0f2a23816e4 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php @@ -58,6 +58,11 @@ */ class CartFixedTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var GuestCartManagementInterface */ @@ -532,11 +537,11 @@ public function testDiscountsWhenByPercentRuleAppliedFirstAndCartFixedRuleSecond $item = array_shift($items); $this->assertEquals('simple1', $item->getSku()); $this->assertEquals(5.99, $item->getPrice()); - $this->assertEquals($expectedDiscounts[$item->getSku()], $item->getDiscountAmount()); + $this->assertEqualsWithDelta($expectedDiscounts[$item->getSku()], $item->getDiscountAmount(), self::EPSILON); $item = array_shift($items); $this->assertEquals('simple2', $item->getSku()); $this->assertEquals(15.99, $item->getPrice()); - $this->assertEquals($expectedDiscounts[$item->getSku()], $item->getDiscountAmount()); + $this->assertEqualsWithDelta($expectedDiscounts[$item->getSku()], $item->getDiscountAmount(), self::EPSILON); } public function discountByPercentDataProvider() diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php index b430d576db3e1..553911cbd8914 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php @@ -6,8 +6,18 @@ namespace Magento\SalesRule\Model\Rule\Condition; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture; +use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Condition\Product\Found; +use Magento\SalesRule\Model\Rule\Condition\Product\Subselect; use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; use Magento\SalesRule\Test\Fixture\ProductFoundInCartConditions as ProductFoundInCartConditionsFixture; use Magento\SalesRule\Test\Fixture\ProductSubselectionInCartConditions as ProductSubselectionInCartConditionsFixture; @@ -34,6 +44,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $fixtures; + /** + * @var QuoteRepository + */ + private $quote; + /** * @inheritDoc */ @@ -41,6 +56,7 @@ protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->quote = $this->objectManager->get(QuoteRepository::class); } /** @@ -197,7 +213,7 @@ public function testValidateParentCategoryWithConfigurable(array $conditions, bo $rule->load($ruleId); $rule->getConditions()->setConditions([])->loadArray( [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'type' => Combine::class, 'attribute' => null, 'operator' => null, 'value' => '1', @@ -226,16 +242,15 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is not "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, + 'type' => Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '!=', @@ -251,16 +266,15 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, + 'type' => Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -276,14 +290,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is not "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '!=', @@ -299,14 +312,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -322,14 +334,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '0', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -343,4 +354,80 @@ public function conditionsDataProvider(): array ], ]; } + + /** + * Ensure that the coupon code shouldn't get applied as the cart contains products from restricted category + * + * @throws NoSuchEntityException + * @return void + */ + #[ + AppIsolation(true), + DbIsolation(true), + DataFixture(CategoryFixture::class, as: 'c1'), + DataFixture(CategoryFixture::class, as: 'c2'), + DataFixture(ProductFixture::class, [ + 'price' => 40, + 'sku' => 'p1', + 'category_ids' => ['$c1.id$'] + ], 'p1'), + DataFixture(ProductFixture::class, [ + 'price' => 30, + 'sku' => 'p2', + 'category_ids' => ['$c2.id$'] + ], 'p2'), + DataFixture( + RuleFixture::class, + [ + 'stop_rules_processing'=> 0, + 'coupon_code' => 'test', + 'discount_amount' => 10, + 'conditions' => [ + [ + 'type' => Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Found::class, + 'value' => '0', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Product::class, + 'attribute' => 'category_ids', + 'operator' => '==', + 'value' => '$c1.id$', + 'is_value_processed' => false, + ], + ], + ], + ], + ], + ], + 'simple_action' => Rule::BY_FIXED_ACTION, + 'sort_order' => 0 + ], + 'rule' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$', 'qty' => 1]), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p2.id$', 'qty' => 1]), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$'], as: 'billingAddress'), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$'], as: 'shippingAddress'), + ] + public function testValidateSalesRuleForRestrictedCategories(): void + { + $cartId = (int)$this->fixtures->get('cart')->getId(); + $quote = $this->quote->get($cartId); + + $ruleId = $this->fixtures->get('rule')->getId(); + $rule = $this->objectManager->create(Rule::class)->load($ruleId); + + $this->assertFalse($rule->validate($quote->getShippingAddress())); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php index 3bb10238989b6..791db5516c57a 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php @@ -128,7 +128,7 @@ public function testSubmitQuoteAndCancelOrder() // Make sure coupon usages value is incremented then order is placed. $order = $this->quoteManagement->submit($quote); - sleep(10); // timeout to processing Magento queue + sleep(30); // timeout to processing Magento queue $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); $coupon->loadByCode($couponCode); @@ -186,7 +186,7 @@ public function testQuoteSubmitFailure(array $mockObjects) try { $quoteManagement->submit($quote); } catch (\Exception $exception) { - sleep(10); // timeout to processing queue + sleep(30); // timeout to processing queue $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); $coupon->loadByCode($couponCode); self::assertEquals( diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php index 964a6248c1c10..7323a5cf3cc22 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php @@ -6,6 +6,8 @@ declare(strict_types=1); +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\SalesRule\Api\RuleRepositoryInterface; @@ -19,11 +21,17 @@ /** @var RuleRepositoryInterface $ruleRepository */ $ruleRepository = $bootstrap->get(RuleRepositoryInterface::class); -$ruleId = $registry->registry('Magento/SalesRule/_files/cart_rule_40_percent_off'); -if ($ruleId) { +$salesRuleName = '40% Off on Large Orders'; +$filterGroup = $bootstrap->get(FilterGroup::class); +$filterGroup->setData('name', $salesRuleName); +$searchCriteria = $bootstrap->create(SearchCriteriaInterface::class); +$searchCriteria->setFilterGroups([$filterGroup]); +$items = $ruleRepository->getList($searchCriteria)->getItems(); +if ($items) { try { - $ruleRepository->deleteById($ruleId); - $registry->unregister('Magento/SalesRule/_files/cart_rule_40_percent_off'); + foreach ($items as $item) { + $ruleRepository->deleteById($item->getRuleId()); + } } catch (NoSuchEntityException $e) { } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php index b2d3df227377d..e5c424ebb9a62 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -$this->_collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( +$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); -$items = array_values($this->_collection->getItems()); +$items = array_values($collection->getItems()); // type SPECIFIC with code $coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php index 764976455ed7d..6e2bf44dd7529 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php @@ -7,10 +7,10 @@ Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/rules_advanced.php'); -$this->_collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( +$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); -$items = array_values($this->_collection->getItems()); +$items = array_values($collection->getItems()); // type SPECIFIC with code $coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php index d3e5d2226a87c..94639ec878684 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php @@ -13,6 +13,7 @@ * Class performs assertion that generated simple products are valid * after running setup:performance:generate-fixtures command */ +#[\AllowDynamicProperties] class SimpleProductsAssert { /** diff --git a/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php b/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php index b8b4ee5de1887..8fc63c702d246 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php @@ -21,6 +21,7 @@ /** * Test that initial scopes config are loaded if database is available * @magentoAppIsolation enabled + * @magentoCache config disabled */ class InitialConfigSourceTest extends TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php new file mode 100644 index 0000000000000..1c19a80e7f35f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model; + +use Magento\Customer\Test\Fixture\Customer; +use Magento\Framework\App\Area; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Security.Superglobal + */ +class MultiStoreTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + + /** + * @inheridoc + * @throws LocalizedException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); + } + + /** + * @return void + * @throws LocalizedException + * @throws \Magento\Framework\Exception\MailException + */ + #[ + DbIsolation(false), + ConfigFixture('system/smtp/transport', 'smtp', 'store'), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + Customer::class, + [ + 'store_id' => '$store2.id$', + 'website_id' => '$website2.id$', + 'addresses' => [[]] + ], + as: 'customer1' + ), + DataFixture(WebsiteFixture::class, as: 'website3'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website3.id$'], 'store_group3'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group3.id$'], 'store3'), + DataFixture( + Customer::class, + [ + 'store_id' => '$store3.id$', + 'website_id' => '$website3.id$', + 'addresses' => [[]] + ], + as: 'customer2' + ), + ] + public function testStoreSpecificEmailInFromHeader() :void + { + $customerOne = $this->fixtures->get('customer1'); + $storeOne = $this->fixtures->get('store2'); + $customerOneData = [ + 'email' => $customerOne->getDataByKey('email'), + 'storeId' => $storeOne->getData('store_id'), + 'storeEmail' => 'store_one@example.com' + ]; + + $this->subscribeNewsLetterAndAssertFromHeader($customerOneData); + + $customerTwo = $this->fixtures->get('customer2'); + $storeTwo = $this->fixtures->get('store3'); + $customerTwoData = [ + 'email' => $customerTwo->getDataByKey('email'), + 'storeId' => $storeTwo->getData('store_id'), + 'storeEmail' => 'store_two@example.com' + ]; + + $this->subscribeNewsLetterAndAssertFromHeader($customerTwoData); + } + + /** + * @param $customerData + * @return void + * @throws LocalizedException + * @throws \Magento\Framework\Exception\MailException + */ + private function subscribeNewsLetterAndAssertFromHeader( + $customerData + ) :void { + /** @var Subscriber $subscriber */ + $subscriber = $this->objectManager->create(Subscriber::class); + $subscriber->subscribe($customerData['email']); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $transportBuilderMock->setTemplateIdentifier( + 'customer_password_reset_password_template' + )->setTemplateVars([ + 'subscriber_data' => [ + 'confirmation_link' => $subscriber->getConfirmationLink(), + ], + ])->setTemplateOptions([ + 'area' => Area::AREA_FRONTEND, + 'store' => (int) $customerData['storeId'] + ]) + ->setFromByScope( + [ + 'email' => $customerData['storeEmail'], + 'name' => 'Store Email Name' + ], + (int) $customerData['storeId'] + ) + ->addTo($customerData['email']) + ->getTransport(); + + $headers = $transportBuilderMock->getSentMessage()->getHeaders(); + + $this->assertNotNull($transportBuilderMock->getSentMessage()); + $this->assertStringContainsString($customerData['storeEmail'], $headers['From']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php index eef8cf960944c..c14ab540f5c9c 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php @@ -24,6 +24,5 @@ if ($store->load('fixture_third_store', 'code')->getId()) { $store->delete(); } - $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php index a119b6259b5f6..f3b43d9f2cca2 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php @@ -23,6 +23,11 @@ */ class TaxTest extends \Magento\TestFramework\Indexer\TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * Utility object for setting up tax rates, tax classes and tax rules * @@ -176,7 +181,7 @@ public function testFullDiscountWithDeltaRoundingFix() protected function verifyItem($item, $expectedItemData) { foreach ($expectedItemData as $key => $value) { - $this->assertEquals($value, $item->getData($key), 'item ' . $key . ' is incorrect'); + $this->assertEqualsWithDelta($value, $item->getData($key), self::EPSILON, 'item ' . $key . ' is incorrect'); } return $this; @@ -247,7 +252,12 @@ protected function verifyQuoteAddress($quoteAddress, $expectedAddressData) if ($key == 'applied_taxes') { $this->verifyAppliedTaxes($quoteAddress->getAppliedTaxes(), $value); } else { - $this->assertEquals($value, $quoteAddress->getData($key), 'Quote address ' . $key . ' is incorrect'); + $this->assertEqualsWithDelta( + $value, + $quoteAddress->getData($key), + self::EPSILON, + 'Quote address ' . $key . ' is incorrect' + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php index 604a444883a26..44633c95f6f99 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php @@ -16,15 +16,16 @@ class TaxCalculationTest extends \PHPUnit\Framework\TestCase { /** - * Object Manager - * + * @var float + */ + private const EPSILON = 0.0000000001; + + /** * @var \Magento\Framework\ObjectManagerInterface */ private $objectManager; /** - * Tax calculation service - * * @var \Magento\Tax\Api\TaxCalculationInterface */ private $taxCalculationService; @@ -108,7 +109,7 @@ public function testCalculateTaxUnitBased($quoteDetailsData, $expected) ); $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails, 1); - $this->assertEquals($expected, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expected, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -1286,7 +1287,7 @@ public function testCalculateTaxRowBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2387,7 +2388,7 @@ public function testMultiRulesRowBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2424,7 +2425,7 @@ public function testMultiRulesTotalBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2471,7 +2472,7 @@ public function testMultiRulesUnitBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index ce271e5102099..0e20b6723b6e0 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -14,13 +14,13 @@ use Magento\Framework\HTTP\AsyncClient\Response; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Quote\Model\Quote\Address\RateRequest; +use Magento\Quote\Model\Quote\Address\RateRequestFactory; use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Shipping\Model\Shipment\Request; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Quote\Model\Quote\Address\RateRequestFactory; use Magento\TestFramework\HTTP\AsyncClientInterfaceMock; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; -use Magento\Shipping\Model\Shipment\Request; +use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; /** @@ -113,29 +113,54 @@ public function testGetShipConfirmUrlLive() } /** - * Collect free rates. + * Collect rates for UPS Ground method. * * @magentoConfigFixture current_store carriers/ups/active 1 - * @magentoConfigFixture current_store carriers/ups/type UPS - * @magentoConfigFixture current_store carriers/ups/allowed_methods 1DA,GND - * @magentoConfigFixture current_store carriers/ups/free_method GND + * @magentoConfigFixture current_store carriers/ups/type UPS_XML + * @magentoConfigFixture current_store carriers/ups/allowed_methods 03 + * @magentoConfigFixture current_store carriers/ups/free_method 03 + * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 + * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the United States + * @magentoConfigFixture default_store carriers/ups/username user + * @magentoConfigFixture default_store carriers/ups/password pass + * @magentoConfigFixture default_store carriers/ups/access_license_number acn + * @magentoConfigFixture default_store carriers/ups/debug 1 + * @magentoConfigFixture default_store currency/options/allow USD,EUR + * @magentoConfigFixture default_store currency/options/base USD */ public function testCollectFreeRates() { - $rateRequest = Bootstrap::getObjectManager()->get(RateRequestFactory::class)->create(); - $rateRequest->setDestCountryId('US'); - $rateRequest->setDestRegionId('CA'); - $rateRequest->setDestPostcode('90001'); - $rateRequest->setPackageQty(1); - $rateRequest->setPackageWeight(1); - $rateRequest->setFreeMethodWeight(0); - $rateRequest->setLimitCarrier($this->carrier::CODE); - $rateRequest->setFreeShipping(true); - $rateResult = $this->carrier->collectRates($rateRequest); - $result = $rateResult->asArray(); - $methods = $result[$this->carrier::CODE]['methods']; - $this->assertEquals(0, $methods['GND']['price']); - $this->assertNotEquals(0, $methods['1DA']['price']); + $request = Bootstrap::getObjectManager()->create( + RateRequest::class, + [ + 'data' => [ + 'dest_country' => 'US', + 'dest_postal' => '90001', + 'package_weight' => '1', + 'package_qty' => '1', + 'free_method_weight' => '5', + 'product' => '11', + 'action' => 'Rate', + 'unit_measure' => 'KGS', + 'free_shipping' => '1', + 'base_currency' => new DataObject(['code' => 'USD']) + ] + ] + ); + //phpcs:disable Magento2.Functions.DiscouragedFunction + $this->httpClient->nextResponses( + [ + new Response( + 200, + [], + file_get_contents(__DIR__ . "/../_files/ups_rates_response_option9.xml") + ) + ] + ); + + $rates = $this->carrier->collectRates($request)->getAllRates(); + $this->assertEquals('19.19', $rates[0]->getPrice()); + $this->assertEquals('03', $rates[0]->getMethod()); } /** @@ -181,7 +206,7 @@ public function testCollectRates(int $negotiable, int $tax, int $responseId, str new Response( 200, [], - file_get_contents(__DIR__ ."/../_files/ups_rates_response_option$responseId.xml") + file_get_contents(__DIR__ . "/../_files/ups_rates_response_option$responseId.xml") ) ] ); @@ -283,9 +308,9 @@ public function collectRatesDataProvider() public function testRequestToShipment(): void { //phpcs:disable Magento2.Functions.DiscouragedFunction - $expectedShipmentRequest = file_get_contents(__DIR__ .'/../_files/ShipmentConfirmRequest.xml'); - $shipmentResponse = file_get_contents(__DIR__ .'/../_files/ShipmentConfirmResponse.xml'); - $acceptResponse = file_get_contents(__DIR__ .'/../_files/ShipmentAcceptResponse.xml'); + $expectedShipmentRequest = file_get_contents(__DIR__ . '/../_files/ShipmentConfirmRequest.xml'); + $shipmentResponse = file_get_contents(__DIR__ . '/../_files/ShipmentConfirmResponse.xml'); + $acceptResponse = file_get_contents(__DIR__ . '/../_files/ShipmentAcceptResponse.xml'); //phpcs:enable Magento2.Functions.DiscouragedFunction $this->httpClient->nextResponses( [ diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option9.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option9.xml new file mode 100644 index 0000000000000..2e5c4bc0ddbfd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option9.xml @@ -0,0 +1,299 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<RatingServiceSelectionResponse> + <Response> + <TransactionReference> + <CustomerContext>Rating and Service</CustomerContext> + <XpciVersion>1.0</XpciVersion> + </TransactionReference> + <ResponseStatusCode>1</ResponseStatusCode> + <ResponseStatusDescription>Success</ResponseStatusDescription> + </Response> + <RatedShipment> + <Service> + <Code>12</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>43.19</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>43.19</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery>3</GuaranteedDaysToDelivery> + <ScheduledDeliveryTime /> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>43.19</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>43.19</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> + <RatedShipment> + <Service> + <Code>14</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>122.34</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>122.34</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> + <ScheduledDeliveryTime>8:00 A.M.</ScheduledDeliveryTime> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>122.34</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>122.34</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> + <RatedShipment> + <Service> + <Code>03</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>19.19</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>19.19</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery /> + <ScheduledDeliveryTime /> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>19.19</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>19.19</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> + <RatedShipment> + <Service> + <Code>13</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>82.24</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>82.24</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> + <ScheduledDeliveryTime /> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>82.24</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>82.24</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> + <RatedShipment> + <Service> + <Code>01</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>88.14</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>88.14</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> + <ScheduledDeliveryTime>10:30 A.M.</ScheduledDeliveryTime> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>88.14</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>88.14</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> + <RatedShipment> + <Service> + <Code>02</Code> + </Service> + <RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>57.11</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>57.11</MonetaryValue> + </TotalCharges> + <GuaranteedDaysToDelivery>2</GuaranteedDaysToDelivery> + <ScheduledDeliveryTime /> + <RatedPackage> + <TransportationCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>57.11</MonetaryValue> + </TransportationCharges> + <ServiceOptionsCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>0.00</MonetaryValue> + </ServiceOptionsCharges> + <TotalCharges> + <CurrencyCode>USD</CurrencyCode> + <MonetaryValue>57.11</MonetaryValue> + </TotalCharges> + <Weight>20.0</Weight> + <BillingWeight> + <UnitOfMeasurement> + <Code>LBS</Code> + </UnitOfMeasurement> + <Weight>20.0</Weight> + </BillingWeight> + </RatedPackage> + </RatedShipment> +</RatingServiceSelectionResponse> 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 6d635d01b2f48..87709053c81b9 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -7,12 +7,14 @@ namespace Magento\UrlRewrite\Model\StoreSwitcher; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\Value; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface as ObjectManager; use Magento\Store\Api\Data\StoreInterface; @@ -20,12 +22,13 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreSwitcher; +use Magento\Store\Model\StoreSwitcher\CannotSwitchStoreException; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * Test store switching + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RewriteUrlTest extends TestCase { @@ -109,12 +112,45 @@ public function testSwitchToExistingPage(): void $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); } + /** + * Testing store switching with existing cms pages with non-existing url keys + * + * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php + * @magentoDbIsolation disabled + * @return void + * @throws StoreSwitcher\CannotSwitchStoreException|NoSuchEntityException + */ + public function testSwitchToExistingPageToNonExistingUrlKeys(): void + { + $fromStore = $this->getStoreByCode('default'); + $toStore = $this->getStoreByCode('fixture_second_store'); + + //test with CMS page with url rewrite for from and target store + $redirectUrl1 = "http://localhost/index.php/page-c/"; + $expectedUrl1 = "http://localhost/index.php/page-c-on-2nd-store"; + + $this->assertEquals($expectedUrl1, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl1)); + + //test with CMS page without url rewrite for second/target store + $redirectUrl2 = "http://localhost/index.php/fixture_second_store/page-e/"; + $expectedUrl2 = "http://localhost/index.php/fixture_second_store/page-e/"; + + $this->assertEquals($expectedUrl2, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl2)); + + //test with custom url rewrite without CMS page + $redirectUrl3 = "http://localhost/index.php/fixture_second_store/contact/"; + $expectedUrl3 = "http://localhost/index.php/fixture_second_store/contact/"; + + $this->assertEquals($expectedUrl3, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl3)); + } + /** * Testing store switching using cms pages with the same url_key but with different page_id * * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDbIsolation disabled * @return void + * @throws CannotSwitchStoreException|NoSuchEntityException */ public function testSwitchCmsPageToAnotherStore(): void { @@ -133,6 +169,9 @@ public function testSwitchCmsPageToAnotherStore(): void * @magentoDbIsolation disabled * @magentoAppArea frontend * @return void + * @throws CannotSwitchStoreException + * @throws NoSuchEntityException + * @throws LocalizedException */ public function testSwitchCmsPageToAnotherStoreAsCustomer(): void { @@ -167,6 +206,7 @@ private function loginAsCustomer($customer) * @param StoreInterface $targetStore * @param string $baseUrl * @return void + * @throws Exception */ private function setBaseUrl(StoreInterface $targetStore, string $baseUrl): void { @@ -189,6 +229,7 @@ private function setBaseUrl(StoreInterface $targetStore, string $baseUrl): void * * @param string $storeCode * @return StoreInterface + * @throws NoSuchEntityException */ private function getStoreByCode(string $storeCode): StoreInterface { diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php index c1b19ca77beb4..f5d4dd5638d28 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php @@ -7,21 +7,28 @@ namespace Magento\User\Controller\Adminhtml; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\Storage\WriterInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Mail\EmailMessage; +use Magento\Framework\Message\MessageInterface; use Magento\Store\Model\Store; use Magento\TestFramework\Fixture\Config as Config; use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\User\Model\User as UserModel; +use Magento\User\Model\UserFactory; use Magento\User\Test\Fixture\User as UserDataFixture; /** * Test class for user reset password email * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml */ class UserResetPasswordEmailTest extends AbstractBackendController @@ -36,6 +43,26 @@ class UserResetPasswordEmailTest extends AbstractBackendController */ protected $userModel; + /** + * @var UserFactory + */ + private $userFactory; + + /** + * @var \Magento\Framework\Mail\MessageInterfaceFactory + */ + private $messageFactory; + + /** + * @var \Magento\Framework\Mail\TransportInterfaceFactory + */ + private $transportFactory; + + /** + * @var WriterInterface + */ + private $configWriter; + /** * @throws LocalizedException */ @@ -44,6 +71,10 @@ protected function setUp(): void parent::setUp(); $this->fixtures = DataFixtureStorageManager::getStorage(); $this->userModel = $this->_objectManager->create(UserModel::class); + $this->messageFactory = $this->_objectManager->get(\Magento\Framework\Mail\MessageInterfaceFactory::class); + $this->transportFactory = $this->_objectManager->get(\Magento\Framework\Mail\TransportInterfaceFactory::class); + $this->userFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(UserFactory::class); + $this->configWriter = $this->_objectManager->get(WriterInterface::class); } #[ @@ -74,4 +105,121 @@ private function getResetPasswordUri(EmailMessage $message): string $urlString = trim($match[0][0], $store->getBaseUrl('web')); return substr($urlString, 0, strpos($urlString, "/key")); } + + /** + * Test admin email notification after password change + * + * @throws LocalizedException + * @return void + */ + #[ + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testAdminEmailNotificationAfterPasswordChange(): void + { + // Load admin user + $user = $this->fixtures->get('user'); + $username = $user->getDataByKey('username'); + $adminEmail = $user->getDataByKey('email'); + + // login with old credentials + $adminUser = $this->userFactory->create(); + $adminUser->login($username, \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + + // Change password + $adminUser->setPassword('newPassword123'); + $adminUser->save(); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + $transportBuilderMock->setTemplateIdentifier( + 'customer_password_reset_password_template' + )->setTemplateVars([ + 'customer' => [ + 'name' => $user->getDataByKey('firstname') . ' ' . $user->getDataByKey('lastname') + ] + ])->setTemplateOptions([ + 'area' => Area::AREA_FRONTEND, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID + ]) + ->addTo($adminEmail) + ->getTransport(); + + $message = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($transportBuilderMock->getSentMessage()); + $this->assertEquals($adminEmail, $message->getTo()[0]->getEmail()); + } + + /** + * @return void + * @throws LocalizedException + */ + #[ + DbIsolation(false), + Config( + 'admin/security/min_time_between_password_reset_requests', + '0', + 'store' + ), + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testEnablePasswordChangeFrequencyLimit(): void + { + // Load admin user + $user = $this->fixtures->get('user'); + $username = $user->getDataByKey('username'); + $adminEmail = $user->getDataByKey('email'); + + // login admin + $adminUser = $this->userFactory->create(); + $adminUser->login($username, \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + } + + /** @var TransportBuilderMock $transportMock */ + $transportMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class + ); + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + // Setting the limit to greater than 0 + $this->configWriter->save('admin/security/min_time_between_password_reset_requests', 2); + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + } + + $this->assertSessionMessages( + $this->equalTo( + ['We received too many requests for password resets.' + . ' Please wait and try again later or contact hello@example.com.'] + ), + MessageInterface::TYPE_ERROR + ); + + // Wait for 2 minutes before resetting password + sleep(120); + + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php new file mode 100644 index 0000000000000..0dfbc6528fcd7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\User\Model; + +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Security\Model\ResourceModel\UserExpiration; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Test\Fixture\User as UserDataFixture; +use Magento\Security\Model\UserExpirationFactory; +use PHPUnit\Framework\TestCase; + +class UserExpirationTest extends TestCase +{ + + /** + * @var UserExpiration + */ + private $userExpirationResource; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + + /** + * @var TimezoneInterface + */ + private $timeZone; + + /** + * @var UserExpiration + */ + private $userExpiration; + + /** + * @var UserExpirationFactory + */ + private $userExpirationFactory; + + /** + * @inheritdoc + * @throws LocalizedException + */ + protected function setUp(): void + { + $this->userExpirationResource = Bootstrap::getObjectManager()->get(UserExpiration::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->userExpirationFactory = Bootstrap::getObjectManager()->get(UserExpirationFactory::class); + $this->timeZone = Bootstrap::getObjectManager()->get(TimezoneInterface::class); + } + + /** + * Verify user expiration saved with large date. + * + * @throws LocalizedException + * @return void + */ + #[ + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testLargeExpirationDate(): void + { + $user = $this->fixtures->get('user'); + $userId = $user->getDataByKey('user_id'); + + // Get date more than 100 years from current date + $initialExpirationDate = $this->timeZone->date()->modify('+100 years'); + $expireDate = $this->timeZone->formatDateTime( + $initialExpirationDate, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + + // Set Expiration date to the admin user and save + $this->setExpirationDateToUser($expireDate, (int)$userId); + + // Load admin expiration date from database + $loadedUserExpiration = $this->userExpirationFactory->create(); + $this->userExpirationResource->load($loadedUserExpiration, $this->userExpiration->getId()); + + self::assertEquals( + strtotime($initialExpirationDate->format('Y-m-d H:i:s')), + strtotime($loadedUserExpiration->getExpiresAt()) + ); + } + + /** + * Set expiration date to admin user and save + * + * @param string $expirationDate + * @param int $userId + * + * @return void + * @throws AlreadyExistsException + */ + private function setExpirationDateToUser(string $expirationDate, int $userId): void + { + $this->userExpiration = $this->userExpirationFactory->create(); + $this->userExpiration->setExpiresAt($expirationDate); + $this->userExpiration->setUserId($userId); + $this->userExpirationResource->save($this->userExpiration); + } +} diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js new file mode 100644 index 0000000000000..c82aeb31d0944 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + + +/* eslint max-nested-callbacks: 0 */ +/* jscs:disable jsDoc*/ + +define([ + 'Magento_ConfigurableProduct/js/variations/steps/summary' +], function (Summary) { + 'use strict'; + + describe('Magento_ConfigurableProduct/js/variations/steps/summary', function () { + let model, quantityFieldName, productDataFromGrid, productDataFromGridExpected; + + beforeEach(function () { + quantityFieldName = 'quantity123'; + model = new Summary({quantityFieldName: quantityFieldName}); + + productDataFromGrid = { + sku: 'testSku', + name: 'test name', + weight: 12.12312, + status: 1, + price: 333.333, + someField: 'someValue', + quantity: 10 + }; + + productDataFromGrid[quantityFieldName] = 12; + + productDataFromGridExpected = { + sku: 'testSku', + name: 'test name', + weight: 12.12312, + status: 1, + price: 333.333 + }; + }); + + describe('Check prepareProductDataFromGrid', function () { + + it('Check call to prepareProductDataFromGrid method with qty', function () { + productDataFromGrid.qty = 3; + productDataFromGridExpected[quantityFieldName] = 3; + const result = model.prepareProductDataFromGrid(productDataFromGrid); + + expect(result).toEqual(productDataFromGridExpected); + }); + + + it('Check call to prepareProductDataFromGrid method without qty', function () { + const result = model.prepareProductDataFromGrid(productDataFromGrid); + + expect(result).toEqual(productDataFromGridExpected); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js index 22465b4e5da8a..b10ac1c433dd6 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js @@ -142,5 +142,22 @@ define([ qtyElement.trigFunc('input'); expect($.fn.trigger).toHaveBeenCalledWith('updatePrice', {}); }); + + it('check if the _configureElement method is enabling configurable option or not', function () { + selectElement.val(14); + widget._configureElement(selectElement); + expect(widget).toBeTruthy(); + }); + + it('check if the _clearSelect method is clearing the option selections or not', function () { + selectElement.empty(); + widget._clearSelect(selectElement); + expect(widget).toBeTruthy(); + }); + + it('check if the _getSimpleProductId method is returning simple product id or not', function () { + widget._getSimpleProductId(selectElement); + expect(widget).toBeTruthy(); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js index e4a2b95a4c975..fed990964659a 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js @@ -160,6 +160,16 @@ define([ jQueryAjax = undefined; }); + it('test that setStoreId calls loadArea with a callback', function () { + init(); + spyOn(order, 'loadArea').and.callFake(function () { + expect(arguments.length).toEqual(4); + expect(arguments[3] instanceof Function).toBeTrue(); + }); + order.setStoreId('id'); + expect(order.loadArea).toHaveBeenCalled(); + }); + describe('Testing the process customer group change', function () { it('and confirm method is called', function () { init(); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js index ba5ad61cfe310..d7516c64fedcb 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js @@ -33,7 +33,13 @@ define([ }, component, dataScope = 'dataScope', - originalJQuery = jQuery.fn; + originalJQuery = jQuery.fn, + params = { + provider: 'provName', + name: '', + index: '', + dataScope: dataScope + }; beforeEach(function (done) { injector.mock(mocks); @@ -41,12 +47,7 @@ define([ 'Magento_Ui/js/form/element/file-uploader', 'knockoutjs/knockout-es5' ], function (Constr) { - component = new Constr({ - provider: 'provName', - name: '', - index: '', - dataScope: dataScope - }); + component = new Constr(params); done(); }); @@ -69,6 +70,40 @@ define([ }); }); + describe('setInitialValue method', function () { + + it('check for chainable', function () { + expect(component.setInitialValue()).toEqual(component); + }); + it('check for set value', function () { + var initialValue = [ + { + 'name': 'test.png', + 'size': 0, + 'type': 'image/png', + 'url': 'http://localhost:8000/media/wysiwyg/test.png' + } + ], expectedValue = [ + { + 'name': 'test.png', + 'size': 2000, + 'type': 'image/png', + 'url': 'http://localhost:8000/media/wysiwyg/test.png' + } + ]; + + spyOn(component, 'setImageSize').and.callFake(function () { + component.value().size = 2000; + }); + spyOn(component, 'getInitialValue').and.returnValue(initialValue); + component.service = true; + expect(component.setInitialValue()).toEqual(component); + expect(component.getInitialValue).toHaveBeenCalled(); + component.setImageSize(initialValue); + expect(component.value().size).toEqual(expectedValue[0].size); + }); + }); + describe('isFileAllowed method', function () { var invalidFile, validFile; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php new file mode 100644 index 0000000000000..f00c9a1eb08c4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// @codingStandardsIgnoreFile +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + `smallint_ref` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint(6) DEFAULT NULL, + `tinyint` tinyint(4) DEFAULT NULL, + `bigint` bigint(20) DEFAULT 0, + `float` float(12,10) DEFAULT 0.0000000000, + `double` double(245,10) DEFAULT 11111111.1111110000, + `decimal` decimal(15,4) DEFAULT 0.0000, + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext DEFAULT NULL, + `mediumtext` mediumtext DEFAULT NULL, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob DEFAULT NULL, + `blob` blob DEFAULT NULL, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int(10) unsigned DEFAULT NULL, + `smallint_main` smallint(6) NOT NULL DEFAULT 0, + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php new file mode 100644 index 0000000000000..b6d4eca91d6a0 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// @codingStandardsIgnoreFile +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + `smallint_ref` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint(6) DEFAULT NULL, + `tinyint` tinyint(4) DEFAULT NULL, + `bigint` bigint(20) DEFAULT 0, + `float` float(12,10) DEFAULT 0.0000000000, + `double` double(245,10) DEFAULT 11111111.1111110000, + `decimal` decimal(15,4) DEFAULT 0.0000, + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext DEFAULT NULL, + `mediumtext` mediumtext DEFAULT NULL, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob DEFAULT NULL, + `blob` blob DEFAULT NULL, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int(10) unsigned DEFAULT NULL, + `smallint_main` smallint(6) NOT NULL DEFAULT 0, + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php new file mode 100644 index 0000000000000..45cb5f6938b41 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// @codingStandardsIgnoreFile +return [ + 'before' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint(6) DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint(6) NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php new file mode 100644 index 0000000000000..c4c9f12fbaeee --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// @codingStandardsIgnoreFile +return [ + 'before' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint(6) DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint(6) NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php new file mode 100644 index 0000000000000..bc469f23f6e2b --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php new file mode 100644 index 0000000000000..403957ca0921d --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php new file mode 100644 index 0000000000000..3c62923cf256f --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'before' => 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php new file mode 100644 index 0000000000000..6568a59de2a33 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'before' => 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb10611.php new file mode 100644 index 0000000000000..4d49221074315 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb10611.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// @codingStandardsIgnoreFile +return ['CREATE TABLE `reference_table` ( +`tinyint_ref` tinyint NOT NULL AUTO_INCREMENT , +`tinyint_without_padding` tinyint NOT NULL DEFAULT 0 , +`bigint_without_padding` bigint NOT NULL DEFAULT 0 , +`smallint_without_padding` smallint NOT NULL DEFAULT 0 , +`integer_without_padding` int NOT NULL DEFAULT 0 , +`smallint_with_big_padding` smallint NOT NULL DEFAULT 0 , +`smallint_without_default` smallint NULL , +`int_without_unsigned` int NULL , +`int_unsigned` int UNSIGNED NULL , +`bigint_default_nullable` bigint UNSIGNED NULL DEFAULT 1 , +`bigint_not_default_not_nullable` bigint UNSIGNED NOT NULL , +CONSTRAINT PRIMARY KEY (`tinyint_ref`) +) ENGINE=innodb DEFAULT CHARSET=utf8mb3 DEFAULT COLLATE=utf8mb3_general_ci + +CREATE TABLE `auto_increment_test` ( +`int_auto_increment_with_nullable` int UNSIGNED NOT NULL AUTO_INCREMENT , +`int_disabled_auto_increment` smallint UNSIGNED NULL DEFAULT 0 , +CONSTRAINT `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` UNIQUE KEY (`int_auto_increment_with_nullable`) +) ENGINE=innodb DEFAULT CHARSET=utf8mb3 DEFAULT COLLATE=utf8mb3_general_ci + +CREATE TABLE `test_table` ( +`smallint` smallint NOT NULL AUTO_INCREMENT , +`tinyint` tinyint NULL , +`bigint` bigint NULL DEFAULT 0 , +`float` float(12, 4) NULL DEFAULT 0 , +`double` decimal(14, 6) NULL DEFAULT 11111111.111111 , +`decimal` decimal(15, 4) NULL DEFAULT 0 , +`date` date NULL , +`timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , +`datetime` datetime NULL DEFAULT 0 , +`longtext` longtext NULL , +`mediumtext` mediumtext NULL , +`varchar` varchar(254) NULL , +`char` char(255) NULL , +`mediumblob` mediumblob NULL , +`blob` blob NULL , +`boolean` BOOLEAN NULL , +CONSTRAINT `TEST_TABLE_SMALLINT_BIGINT` UNIQUE KEY (`smallint`,`bigint`), +CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE NO ACTION, +INDEX `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`) +) ENGINE=innodb DEFAULT CHARSET=utf8mb3 DEFAULT COLLATE=utf8mb3_general_ci + +CREATE TABLE `patch_list` ( +`patch_id` int NOT NULL AUTO_INCREMENT COMMENT "Patch Auto Increment", +`patch_name` varchar(1024) NOT NULL COMMENT "Patch Class Name", +CONSTRAINT PRIMARY KEY (`patch_id`) +) ENGINE=innodb DEFAULT CHARSET=utf8mb3 DEFAULT COLLATE=utf8mb3_general_ci COMMENT="List of data/schema patches" + +']; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema.xml new file mode 100644 index 0000000000000..5aef6dfc09f93 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="test_table" resource="default"> + <!--Columns--> + <column xsi:type="tinyint" name="tinyint" disabled="true"/> + <column xsi:type="tinyint" name="customint" unsigned="false" nullable="true" + onCreate="migrateDataFrom(tinyint)" comment="Version Id"/> + + <!--Constraints--> + <constraint xsi:type="foreign" referenceId="TEST_TABLE_TINYINT_REFERENCE" disabled="1"/> + <constraint xsi:type="foreign" referenceId="TEST_TABLE_CUSTOMINT_REFERENCE" + column="customint" table="test_table" + referenceTable="reference_table" referenceColumn="tinyint_ref" onDelete="NO ACTION"/> + <!--Indexes--> + <index referenceId="TEST_TABLE_INDEX" indexType="btree" disabled="1"/> + <index referenceId="TEST_TABLE_CUSTOMINT_INDEX" indexType="btree"> + <column name="customint"/> + <column name="bigint"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..8d164bdc311ca --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json @@ -0,0 +1,16 @@ +{ + "test_table": { + "column": { + "tinyint": true, + "customint": true + }, + "index": { + "TEST_TABLE_TINYINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_REFERENCE": true + }, + "constraint": { + "TEST_TABLE_INDEX": true, + "TEST_TABLE_CUSTOMINT_INDEX": true + } + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml new file mode 100644 index 0000000000000..dbf9f9019ca8e --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestSetupDeclarationModule10" setup_version="0.0.1"/> + <sequence> + <module name="Magento_TestSetupDeclarationModule1"/> + </sequence> +</config> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php new file mode 100644 index 0000000000000..6834a540e7d6f --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule10') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule10', __DIR__); +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml new file mode 100644 index 0000000000000..5aef6dfc09f93 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="test_table" resource="default"> + <!--Columns--> + <column xsi:type="tinyint" name="tinyint" disabled="true"/> + <column xsi:type="tinyint" name="customint" unsigned="false" nullable="true" + onCreate="migrateDataFrom(tinyint)" comment="Version Id"/> + + <!--Constraints--> + <constraint xsi:type="foreign" referenceId="TEST_TABLE_TINYINT_REFERENCE" disabled="1"/> + <constraint xsi:type="foreign" referenceId="TEST_TABLE_CUSTOMINT_REFERENCE" + column="customint" table="test_table" + referenceTable="reference_table" referenceColumn="tinyint_ref" onDelete="NO ACTION"/> + <!--Indexes--> + <index referenceId="TEST_TABLE_INDEX" indexType="btree" disabled="1"/> + <index referenceId="TEST_TABLE_CUSTOMINT_INDEX" indexType="btree"> + <column name="customint"/> + <column name="bigint"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php new file mode 100644 index 0000000000000..2ec165aedf085 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'test_table_one' => 'CREATE TABLE `test_table_one` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php new file mode 100644 index 0000000000000..1a1b02dce67cd --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'test_table_one' => 'CREATE TABLE `test_table_one` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php new file mode 100644 index 0000000000000..bc469f23f6e2b --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php new file mode 100644 index 0000000000000..403957ca0921d --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Annotation/DataProviderFromFile.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Annotation/DataProviderFromFile.php index b6e1a877b00cd..ce4ac8b360514 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Annotation/DataProviderFromFile.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Annotation/DataProviderFromFile.php @@ -29,7 +29,9 @@ class DataProviderFromFile SqlVersionProvider::MYSQL_8_0_VERSION => 'mysql8', SqlVersionProvider::MARIA_DB_10_4_VERSION => 'mariadb10', SqlVersionProvider::MARIA_DB_10_6_VERSION => 'mariadb106', - SqlVersionProvider::MYSQL_8_0_29_VERSION => 'mysql829' + SqlVersionProvider::MYSQL_8_0_29_VERSION => 'mysql829', + SqlVersionProvider::MARIA_DB_10_4_27_VERSION => 'mariadb10427', + SqlVersionProvider::MARIA_DB_10_6_11_VERSION => 'mariadb10611' ]; /** diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php b/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php index e69d109be0719..d58f1c06a8a05 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php @@ -110,6 +110,12 @@ private function getDbKey(): string if ($this->sqlVersionProvider->isMysqlGte8029()) { $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MYSQL_8_0_29_VERSION]; break; + } elseif ($this->sqlVersionProvider->isMariaDBGte10427()) { + $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MARIA_DB_10_4_27_VERSION]; + break; + } elseif ($this->sqlVersionProvider->isMariaDBGte10611()) { + $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MARIA_DB_10_6_11_VERSION]; + break; } elseif (strpos($this->getDatabaseVersion(), (string)$possibleVersion) !== false) { $this->dbKey = $suffix; break; diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php index d16854a6eae88..08c57690b83ba 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php @@ -70,6 +70,7 @@ protected function setUp(): void * * @moduleName Magento_TestSetupDeclarationModule1 * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule10 * @throws \Exception */ public function testExecute() @@ -77,6 +78,7 @@ public function testExecute() $modules = [ 'Magento_TestSetupDeclarationModule1', 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule10', ]; $this->cliCommand->install($modules); @@ -114,7 +116,7 @@ private function checkWhitelistFile(string $moduleName) $this->assertEmpty($this->tester->getDisplay()); $whitelistFileContent = file_get_contents($whiteListFileName); - $expectedWhitelistContent = file_get_contents( + $expectedWhitelistContent = rtrim(file_get_contents( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . implode( @@ -126,7 +128,7 @@ private function checkWhitelistFile(string $moduleName) 'db_schema_whitelist.json' ] ) - ); + ), "\n"); $this->assertEquals($expectedWhitelistContent, $whitelistFileContent); } } diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json new file mode 100644 index 0000000000000..886fbb2aaa21f --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json @@ -0,0 +1,18 @@ +{ + "test_table": { + "column": { + "tinyint": true, + "customint": true + }, + "index": { + "TEST_TABLE_TINYINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_BIGINT": true + }, + "constraint": { + "TEST_TABLE_INDEX": true, + "TEST_TABLE_CUSTOMINT_INDEX": true, + "TEST_TABLE_CUSTOMINT_REFERENCE_TABLE_TINYINT_REF": true + } + } +} diff --git a/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php b/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php index dc27b019f4c55..3de9a61a99c6e 100644 --- a/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php +++ b/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php @@ -66,14 +66,17 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in return self::NO_ERRORS; } + //@phpstan:ignore-line $clearedAnalysisResult = new AnalysisResult( $this->clearIgnoredErrors($analysisResult->getFileSpecificErrors()), $analysisResult->getNotFileSpecificErrors(), $analysisResult->getInternalErrors(), $analysisResult->getWarnings(), + $analysisResult->getCollectedData(), $analysisResult->isDefaultLevelUsed(), $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved() + $analysisResult->isResultCacheSaved(), + $analysisResult->getPeakMemoryUsageBytes() ); return $this->tableErrorFormatter->formatErrors($clearedAnalysisResult, $output); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php index cb9f9bbadeea3..8937fcda2090c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php @@ -1,7 +1,5 @@ <?php /** - * Test layout declaration and usage of block elements - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -70,7 +68,7 @@ function ($alias, $file) { ) ); } else { - $this->markTestIncomplete( + $this->markTestSkipped( "Element with alias '{$alias}' is used as a block in file '{$file}' " . "via getChildBlock() method." . " It's impossible to determine explicitly whether the element is a block or a container, " . diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php index 0ecaf496dad7e..45852174d4e35 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php @@ -6,6 +6,7 @@ namespace Magento\Test\Integrity; use Magento\Framework\App\Utility\Files; +use Magento\Tax\Observer\GetPriceConfigurationObserver; /** * PAY ATTENTION: Current implementation does not support of virtual types @@ -13,9 +14,9 @@ class ObserverImplementationTest extends \PHPUnit\Framework\TestCase { /** - * Observer interface + * @var string */ - const OBSERVER_INTERFACE = \Magento\Framework\Event\ObserverInterface::class; + public const OBSERVER_INTERFACE = \Magento\Framework\Event\ObserverInterface::class; /** * @var array @@ -56,9 +57,16 @@ public function testObserverHasNoExtraPublicMethods() $errors = []; foreach (self::$observerClasses as $observerClass) { $reflection = (new \ReflectionClass($observerClass)); + $publicMethodsCount = 0; $maxCountMethod = $reflection->getConstructor() ? 2 : 1; + $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); + foreach ($publicMethods as $publicMethod) { + if (!str_starts_with($publicMethod->getName(), '_')) { + $publicMethodsCount++; + } + } - if (count($reflection->getMethods(\ReflectionMethod::IS_PUBLIC)) > $maxCountMethod) { + if ($publicMethodsCount > $maxCountMethod) { $errors[] = $observerClass; } } @@ -97,6 +105,7 @@ protected static function getObserverClasses($fileNamePattern, $xpath) $blacklistFiles = str_replace('\\', '/', realpath(__DIR__)) . '/_files/blacklist/observers*.txt'; $blacklistExceptions = []; foreach (glob($blacklistFiles) as $fileName) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $blacklistExceptions = array_merge( $blacklistExceptions, file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/allowed_dependencies/ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/allowed_dependencies/ce.php index 278bff7f0abf3..b4506499d0044 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/allowed_dependencies/ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/allowed_dependencies/ce.php @@ -8,7 +8,6 @@ return [ 'Magento\Elasticsearch' => [ 'Magento\Elasticsearch7', - 'Magento\Elasticsearch8', 'Magento\OpenSearch' ] ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php index e99ce8fb84adf..df09ad58276f7 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php @@ -22,11 +22,14 @@ class LegacyFixtureTest extends TestCase */ public function testNew(): void { - $docUrl = 'https://devdocs.magento.com/guides/v2.4/test/integration/parameterized_data_fixture.html'; + $docUrl = 'https://developer.adobe.com/commerce/testing/guide/integration/attributes/data-fixture/'; $files = AddedFiles::getAddedFilesList(__DIR__ . '/..'); $legacyFixtureFiles = []; + //pattern to ignore skip and filter files + $skip_pattern = '/(.*(filter|skip)-list(_ee|_b2b|).php)/'; foreach ($files as $file) { if (pathinfo($file, PATHINFO_EXTENSION) === 'php' + && !preg_match($skip_pattern, $file) && ( preg_match('/(integration\/testsuite|api-functional\/testsuite).*\/(_files|Fixtures)/', $file) // Cover the case when tests are located in the module folder instead of dev/tests. diff --git a/dev/tests/unit/phpunit.xml.dist b/dev/tests/unit/phpunit.xml.dist index c9c576c0adc3a..e185bd1085b51 100644 --- a/dev/tests/unit/phpunit.xml.dist +++ b/dev/tests/unit/phpunit.xml.dist @@ -57,7 +57,7 @@ <!-- Optional arguments block; omit it if you want to use default values --> <arguments> <!-- Path to config file (default is config/allure.config.php) --> - <string>allure/allure.config.php</string> + <directory>allure/allure.config.php</directory> </arguments> </extension> </extensions> diff --git a/dev/tools/grunt/configs/less.js b/dev/tools/grunt/configs/less.js index 473708d3301be..9ae376b9e21ba 100644 --- a/dev/tools/grunt/configs/less.js +++ b/dev/tools/grunt/configs/less.js @@ -22,6 +22,10 @@ var lessOptions = { sourceMap: true, strictImports: false, sourceMapRootpath: '/', + sourceMapBasepath: function (f) { + this.sourceMapURL = this.sourceMapFilename.substr(this.sourceMapFilename.lastIndexOf('/') + 1); + return "/"; + }, dumpLineNumbers: false, // use 'comments' instead false to output line comments for source ieCompat: false }, diff --git a/lib/internal/Magento/Framework/Acl/Builder.php b/lib/internal/Magento/Framework/Acl/Builder.php index 03adaca0589ce..6e380c90f443c 100644 --- a/lib/internal/Magento/Framework/Acl/Builder.php +++ b/lib/internal/Magento/Framework/Acl/Builder.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Acl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Access Control List Builder. Retrieves required role/rule/resource loaders * and uses them to populate provided ACL object. Acl object is put to cache after creation. @@ -13,7 +15,7 @@ * @api * @since 100.0.2 */ -class Builder +class Builder implements ResetAfterRequestInterface { /** * Acl object @@ -85,4 +87,12 @@ public function resetRuntimeAcl() $this->_acl = null; return $this; } + + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->resetRuntimeAcl(); + } } diff --git a/lib/internal/Magento/Framework/Amqp/Config.php b/lib/internal/Magento/Framework/Amqp/Config.php index fa0d9072c4982..55058147c6b55 100644 --- a/lib/internal/Magento/Framework/Amqp/Config.php +++ b/lib/internal/Magento/Framework/Amqp/Config.php @@ -116,7 +116,11 @@ public function __construct( */ public function __destruct() { - $this->closeConnection(); + try { + $this->closeConnection(); + } catch (\Throwable $e) { + error_log($e->getMessage()); + } } /** diff --git a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php index fb0533bdf9c49..463217292363e 100644 --- a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php +++ b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php @@ -7,12 +7,14 @@ namespace Magento\Framework\Api; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Base Builder Class for simple data Objects - * @deprecated 103.0.0 Every builder should have their own implementation of \Magento\Framework\Api\SimpleBuilderInterface + * @deprecated 103.0.0 Every builder should have own implementation of \Magento\Framework\Api\SimpleBuilderInterface * @SuppressWarnings(PHPMD.NumberOfChildren) */ -abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface +abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface, ResetAfterRequestInterface { /** * @var array @@ -85,4 +87,12 @@ public function getData() { return $this->data; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/lib/internal/Magento/Framework/Api/DataObjectHelper.php b/lib/internal/Magento/Framework/Api/DataObjectHelper.php index 27c1e46e3815d..d4acb4c8406e9 100644 --- a/lib/internal/Magento/Framework/Api/DataObjectHelper.php +++ b/lib/internal/Magento/Framework/Api/DataObjectHelper.php @@ -102,17 +102,14 @@ protected function _setDataValues($dataObject, array $data, $interfaceName) return $this; } $setMethods = $this->getSetters($dataObject); - if ($dataObject instanceof ExtensibleDataInterface - && !empty($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) - ) { - foreach ($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $customAttribute) { - $dataObject->setCustomAttribute( - $customAttribute[AttributeInterface::ATTRIBUTE_CODE], - $customAttribute[AttributeInterface::VALUE] - ); - } - unset($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]); - } + $data = $this->setCustomAttributes( + $dataObject, + $data, + [ + CustomAttributesDataInterface::CUSTOM_ATTRIBUTES, + CustomAttributesDataInterface::CUSTOM_ATTRIBUTES . "V2" + ] + ); if ($dataObject instanceof \Magento\Framework\Model\AbstractModel) { $simpleData = array_filter($data, static function ($e) { return is_scalar($e) || is_null($e); @@ -316,4 +313,30 @@ private function getSetters(object $dataObject): array } return $this->settersCache[$class]; } + + /** + * Set custom attributes using the $attributeKeys parameter. + * + * @param mixed $dataObject + * @param array $data + * @param array $attributeKeys + * @return array + */ + public function setCustomAttributes(mixed $dataObject, array $data, array $attributeKeys): array + { + foreach ($attributeKeys as $attributeKey) { + if ($dataObject instanceof ExtensibleDataInterface + && !empty($data[$attributeKey]) + ) { + foreach ($data[$attributeKey] as $customAttribute) { + $dataObject->setCustomAttribute( + $customAttribute[AttributeInterface::ATTRIBUTE_CODE], + $customAttribute[AttributeInterface::VALUE] + ); + } + unset($data[$attributeKey]); + } + } + return $data; + } } diff --git a/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php b/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php index 896812f38c280..e5cf9d7c54886 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php @@ -71,6 +71,8 @@ public function addFilters(array $filter) } /** + * Add search filter + * * @param string $field * @param mixed $value * @param string $conditionType diff --git a/lib/internal/Magento/Framework/App/Action/Forward.php b/lib/internal/Magento/Framework/App/Action/Forward.php index c81bc48ace4d5..5f043fbf771af 100644 --- a/lib/internal/Magento/Framework/App/Action/Forward.php +++ b/lib/internal/Magento/Framework/App/Action/Forward.php @@ -1,13 +1,13 @@ <?php /** - * Forward action class - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\App\Action; +use Magento\Framework\App\CsrfAwareActionInterface; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Request\InvalidRequestException; use Magento\Framework\App\ResponseInterface; /** @@ -15,11 +15,10 @@ * * @SuppressWarnings(PHPMD.AllPurposeAction) */ -class Forward extends AbstractAction +class Forward extends AbstractAction implements CsrfAwareActionInterface { /** - * @param RequestInterface $request - * @return ResponseInterface + * @inheritDoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function dispatch(RequestInterface $request) @@ -35,4 +34,21 @@ public function execute() $this->_request->setDispatched(false); return $this->_response; } + + /** + * @inheritDoc + */ + public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException + { + return new InvalidRequestException($this->_response); + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + // This exists so that we can forward to the noroute action in the admin + return true; + } } diff --git a/lib/internal/Magento/Framework/App/ActionFlag.php b/lib/internal/Magento/Framework/App/ActionFlag.php index 3d6c2756595ad..b028b71c299c9 100644 --- a/lib/internal/Magento/Framework/App/ActionFlag.php +++ b/lib/internal/Magento/Framework/App/ActionFlag.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\App; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Request processing flag that allows to stop request dispatching in action controller from an observer * Downside of this approach is temporal coupling and global communication. @@ -15,7 +17,7 @@ * @api * @since 100.0.2 */ -class ActionFlag +class ActionFlag implements ResetAfterRequestInterface { /** * @var RequestInterface @@ -38,9 +40,9 @@ public function __construct(\Magento\Framework\App\RequestInterface $request) /** * Setting flag value * - * @param string $action - * @param string $flag - * @param string $value + * @param string $action + * @param string $flag + * @param string $value * @return void */ public function set($action, $flag, $value) @@ -83,4 +85,12 @@ protected function _getControllerKey() { return $this->_request->getRouteName() . '_' . $this->_request->getControllerName(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_flags = []; + } } diff --git a/lib/internal/Magento/Framework/App/Area.php b/lib/internal/Magento/Framework/App/Area.php index ea8f96e0c0467..58a690a230b59 100644 --- a/lib/internal/Magento/Framework/App/Area.php +++ b/lib/internal/Magento/Framework/App/Area.php @@ -18,24 +18,24 @@ */ class Area implements \Magento\Framework\App\AreaInterface { - const AREA_GLOBAL = 'global'; - const AREA_FRONTEND = 'frontend'; - const AREA_ADMINHTML = 'adminhtml'; - const AREA_DOC = 'doc'; - const AREA_CRONTAB = 'crontab'; - const AREA_WEBAPI_REST = 'webapi_rest'; - const AREA_WEBAPI_SOAP = 'webapi_soap'; - const AREA_GRAPHQL = 'graphql'; + public const AREA_GLOBAL = 'global'; + public const AREA_FRONTEND = 'frontend'; + public const AREA_ADMINHTML = 'adminhtml'; + public const AREA_DOC = 'doc'; + public const AREA_CRONTAB = 'crontab'; + public const AREA_WEBAPI_REST = 'webapi_rest'; + public const AREA_WEBAPI_SOAP = 'webapi_soap'; + public const AREA_GRAPHQL = 'graphql'; /** * @deprecated */ - const AREA_ADMIN = 'admin'; + public const AREA_ADMIN = 'admin'; /** * Area parameter. */ - const PARAM_AREA = 'area'; + public const PARAM_AREA = 'area'; /** * Array of area loaded parts @@ -52,22 +52,16 @@ class Area implements \Magento\Framework\App\AreaInterface protected $_code; /** - * Event Manager - * * @var \Magento\Framework\Event\ManagerInterface */ protected $_eventManager; /** - * Translator - * * @var \Magento\Framework\TranslateInterface */ protected $_translator; /** - * Object manager - * * @var \Magento\Framework\ObjectManagerInterface */ protected $_objectManager; @@ -189,6 +183,8 @@ protected function _applyUserAgentDesignException($request) } /** + * Get Design instance + * * @return \Magento\Framework\View\DesignInterface */ protected function _getDesign() diff --git a/lib/internal/Magento/Framework/App/AreaList.php b/lib/internal/Magento/Framework/App/AreaList.php index 8c3cc1d551916..c69bf42bdc06a 100644 --- a/lib/internal/Magento/Framework/App/AreaList.php +++ b/lib/internal/Magento/Framework/App/AreaList.php @@ -5,12 +5,14 @@ */ namespace Magento\Framework\App; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Lists router area codes & processes resolves FrontEndNames to area codes * * @api */ -class AreaList +class AreaList implements ResetAfterRequestInterface { /** * @var array @@ -127,4 +129,12 @@ public function getArea($code) } return $this->_areaInstances[$code]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_areaInstances = []; + } } diff --git a/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php b/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php index a11debd80bb40..4c6aca2d6ef55 100644 --- a/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php +++ b/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php @@ -34,12 +34,12 @@ class Factory /** * Default cache entry lifetime */ - const DEFAULT_LIFETIME = 7200; + public const DEFAULT_LIFETIME = 7200; /** * Caching params, that applied for all cache frontends regardless of type */ - const PARAM_CACHE_FORCED_OPTIONS = 'cache_options'; + public const PARAM_CACHE_FORCED_OPTIONS = 'cache_options'; /** * @var ObjectManagerInterface @@ -87,8 +87,6 @@ class Factory ]; /** - * Resource - * * @var ResourceConnection */ protected $_resource; @@ -229,7 +227,7 @@ private function _applyDecorators(FrontendInterface $frontend) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function _getBackendOptions(array $cacheOptions) + protected function _getBackendOptions(array $cacheOptions) //phpcs:ignore Generic.Metrics.NestingLevel { $enableTwoLevels = false; $type = isset($cacheOptions['backend']) ? $cacheOptions['backend'] : $this->_defaultBackend; @@ -302,6 +300,7 @@ protected function _getBackendOptions(array $cacheOptions) $backendType = $type; } } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (Exception $e) { } } @@ -445,4 +444,15 @@ private function createCacheWithDefaultOptions(array $options): Zend ] ); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Cache/README.md b/lib/internal/Magento/Framework/App/Cache/README.md index 4f6964d150fef..43bbcbf8deec1 100644 --- a/lib/internal/Magento/Framework/App/Cache/README.md +++ b/lib/internal/Magento/Framework/App/Cache/README.md @@ -1,6 +1,7 @@ Components of Magento application use caches in their implementation. The **Magento\Cache** library provides an interface for cache storage and segmentation (a.k.a. "types"). **Magento\Framework\App\Cache** extends **Magento\Cache** and provides more specific features: + * State of cache segments (enabled/disabled) and managing their state * Pool of cache frontends * List of cache segments (types) diff --git a/lib/internal/Magento/Framework/App/Cache/Type/Config.php b/lib/internal/Magento/Framework/App/Cache/Type/Config.php index 9ba4b269d21a3..10504660f4cc9 100644 --- a/lib/internal/Magento/Framework/App/Cache/Type/Config.php +++ b/lib/internal/Magento/Framework/App/Cache/Type/Config.php @@ -20,12 +20,12 @@ class Config extends TagScope implements CacheInterface /** * Cache type code unique among all cache types */ - const TYPE_IDENTIFIER = 'config'; + public const TYPE_IDENTIFIER = 'config'; /** * Cache tag used to distinguish the cache type from all other cache */ - const CACHE_TAG = 'CONFIG'; + public const CACHE_TAG = 'CONFIG'; /** * @var \Magento\Framework\App\Cache\Type\FrontendPool @@ -64,4 +64,15 @@ public function getTag() { return self::CACHE_TAG; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Config.php b/lib/internal/Magento/Framework/App/Config.php index 5d8d50bcb909e..32a1cd4948d66 100644 --- a/lib/internal/Magento/Framework/App/Config.php +++ b/lib/internal/Magento/Framework/App/Config.php @@ -1,6 +1,5 @@ <?php /** - * Application configuration object. Used to access configuration when application is initialized and installed. * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -12,14 +11,14 @@ use Magento\Framework\App\Config\ScopeConfigInterface; /** - * Class Config + * Application configuration object. Used to access configuration when application is initialized and installed. */ class Config implements ScopeConfigInterface { /** * Config cache tag */ - const CACHE_TAG = 'CONFIG'; + public const CACHE_TAG = 'CONFIG'; /** * @var ScopeCodeResolver @@ -134,4 +133,15 @@ public function get($configType, $path = '', $default = null) return $result !== null ? $result : $default; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig.php b/lib/internal/Magento/Framework/App/DeploymentConfig.php index 6713baa3a1d54..a14354b4fdb9a 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig.php @@ -51,6 +51,16 @@ class DeploymentConfig */ private $overrideData; + /** + * @var array + */ + private $envOverrides = []; + + /** + * @var array + */ + private $readerLoad = []; + /** * Constructor * @@ -84,7 +94,9 @@ public function get($key = null, $defaultValue = null) } $result = $this->getByKey($key); if ($result === null) { - $this->reloadData(); + if (empty($this->flatData) || count($this->getAllEnvOverrides())) { + $this->reloadData(); + } $result = $this->getByKey($key); } return $result ?? $defaultValue; @@ -114,13 +126,13 @@ public function getConfigData($key = null) { if ($key === null) { if (empty($this->data)) { - $this->reloadData(); + $this->reloadInitialData(); } return $this->data; } $result = $this->getConfigDataByKey($key); if ($result === null) { - $this->reloadData(); + $this->reloadInitialData(); $result = $this->getConfigDataByKey($key); } return $result; @@ -170,28 +182,55 @@ private function getEnvOverride() : array * @throws FileSystemException * @throws RuntimeException */ - private function reloadData(): void + private function reloadInitialData(): void { + if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { + $this->readerLoad = $this->reader->load(); + } $this->data = array_replace( - $this->reader->load(), + $this->readerLoad, $this->overrideData ?? [], $this->getEnvOverride() ); + } + + /** + * Loads the configuration data + * + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + private function reloadData(): void + { + $this->reloadInitialData(); // flatten data for config retrieval using get() $this->flatData = $this->flattenParams($this->data); + $this->flatData = $this->getAllEnvOverrides() + $this->flatData; + } - // allow reading values from env variables by convention - // MAGENTO_DC_{path}, like db/connection/default/host => - // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST - foreach (getenv() as $key => $value) { - if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) - && $key !== self::OVERRIDE_KEY - ) { - // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host - $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); - $this->flatData[$flatKey] = $value; + /** + * Load all getenv() configs once + * + * @return array + */ + private function getAllEnvOverrides(): array + { + if (empty($this->envOverrides)) { + // allow reading values from env variables by convention + // MAGENTO_DC_{path}, like db/connection/default/host => + // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST + foreach (getenv() as $key => $value) { + if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) + && $key !== self::OVERRIDE_KEY + ) { + // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host + $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); + $this->envOverrides[$flatKey] = $value; + } } } + return $this->envOverrides; } /** @@ -266,4 +305,15 @@ private function getConfigDataByKey(?string $key) { return $this->data[$key] ?? null; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Http/Context.php b/lib/internal/Magento/Framework/App/Http/Context.php index b3fa5a5cca67b..6c4648be087ff 100644 --- a/lib/internal/Magento/Framework/App/Http/Context.php +++ b/lib/internal/Magento/Framework/App/Http/Context.php @@ -8,6 +8,7 @@ namespace Magento\Framework\App\Http; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; /** @@ -15,12 +16,12 @@ * * @api */ -class Context +class Context implements ResetAfterRequestInterface { /** * Currency cache context */ - const CONTEXT_CURRENCY = 'current_currency'; + public const CONTEXT_CURRENCY = 'current_currency'; /** * Data storage @@ -134,4 +135,13 @@ public function toArray() 'default' => $this->default ]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + $this->default = []; + } } diff --git a/lib/internal/Magento/Framework/App/PageCache/Identifier.php b/lib/internal/Magento/Framework/App/PageCache/Identifier.php index 10901e418963c..b0ef3345f98ab 100644 --- a/lib/internal/Magento/Framework/App/PageCache/Identifier.php +++ b/lib/internal/Magento/Framework/App/PageCache/Identifier.php @@ -11,7 +11,7 @@ /** * Page unique identifier */ -class Identifier +class Identifier implements IdentifierInterface { /** * @var \Magento\Framework\App\Request\Http diff --git a/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php b/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php new file mode 100644 index 0000000000000..e3b92b241aaed --- /dev/null +++ b/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\App\PageCache; + +/** + * Page unique identifier interface + */ +interface IdentifierInterface +{ + /** + * Return unique page identifier + * + * @return string + */ + public function getValue(); +} diff --git a/lib/internal/Magento/Framework/App/PageCache/Kernel.php b/lib/internal/Magento/Framework/App/PageCache/Kernel.php index b75a942b2d0b8..c4d88f031a512 100644 --- a/lib/internal/Magento/Framework/App/PageCache/Kernel.php +++ b/lib/internal/Magento/Framework/App/PageCache/Kernel.php @@ -10,6 +10,8 @@ /** * Builtin cache processor + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Kernel { @@ -17,11 +19,12 @@ class Kernel * @var \Magento\PageCache\Model\Cache\Type * * @deprecated 100.1.0 + * @see Nothing */ protected $cache; /** - * @var Identifier + * @var \Magento\Framework\App\PageCache\IdentifierInterface */ protected $identifier; @@ -60,9 +63,14 @@ class Kernel */ private $state; + /** + * @var \Magento\Framework\App\PageCache\IdentifierInterface + */ + private $identifierForSave; + /** * @param Cache $cache - * @param Identifier $identifier + * @param \Magento\Framework\App\PageCache\IdentifierInterface $identifier * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\App\Http\Context|null $context * @param \Magento\Framework\App\Http\ContextFactory|null $contextFactory @@ -70,17 +78,20 @@ class Kernel * @param \Magento\Framework\Serialize\SerializerInterface|null $serializer * @param AppState|null $state * @param \Magento\PageCache\Model\Cache\Type|null $fullPageCache + * @param \Magento\Framework\App\PageCache\IdentifierInterface|null $identifierForSave + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\PageCache\Cache $cache, - \Magento\Framework\App\PageCache\Identifier $identifier, + \Magento\Framework\App\PageCache\IdentifierInterface $identifier, \Magento\Framework\App\Request\Http $request, \Magento\Framework\App\Http\Context $context = null, \Magento\Framework\App\Http\ContextFactory $contextFactory = null, \Magento\Framework\App\Response\HttpFactory $httpFactory = null, \Magento\Framework\Serialize\SerializerInterface $serializer = null, AppState $state = null, - \Magento\PageCache\Model\Cache\Type $fullPageCache = null + \Magento\PageCache\Model\Cache\Type $fullPageCache = null, + \Magento\Framework\App\PageCache\IdentifierInterface $identifierForSave = null ) { $this->cache = $cache; $this->identifier = $identifier; @@ -99,6 +110,9 @@ public function __construct( $this->fullPageCache = $fullPageCache ?? ObjectManager::getInstance()->get( \Magento\PageCache\Model\Cache\Type::class ); + $this->identifierForSave = $identifierForSave ?? ObjectManager::getInstance()->get( + \Magento\Framework\App\PageCache\IdentifierInterface::class + ); } /** @@ -128,13 +142,18 @@ public function load() * * @param \Magento\Framework\App\Response\Http $response * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function process(\Magento\Framework\App\Response\Http $response) { - if (preg_match('/public.*s-maxage=(\d+)/', $response->getHeader('Cache-Control')->getFieldValue(), $matches)) { + $cacheControlHeader = $response->getHeader('Cache-Control'); + if ($cacheControlHeader + && preg_match('/public.*s-maxage=(\d+)/', $cacheControlHeader->getFieldValue(), $matches) + ) { $maxAge = $matches[1]; $response->setNoCacheHeaders(); if (($response->getHttpResponseCode() == 200 || $response->getHttpResponseCode() == 404) + && !$response instanceof NotCacheableInterface && ($this->request->isGet() || $this->request->isHead()) ) { $tagsHeader = $response->getHeader('X-Magento-Tags'); @@ -150,7 +169,7 @@ public function process(\Magento\Framework\App\Response\Http $response) $this->fullPageCache->save( $this->serializer->serialize($this->getPreparedData($response)), - $this->identifier->getValue(), + $this->identifierForSave->getValue(), $tags, $maxAge ); diff --git a/lib/internal/Magento/Framework/App/README.md b/lib/internal/Magento/Framework/App/README.md index 87665be48425a..99736f9f8018a 100644 --- a/lib/internal/Magento/Framework/App/README.md +++ b/lib/internal/Magento/Framework/App/README.md @@ -3,16 +3,17 @@ Unlike other components of **Magento\Framework** that are generic libraries not specific to Magento application, the **Magento\Framework\App** is "aware of" Magento application intentionally. The library implements a variety of features of the Magento application: - * bootstrap and initialization parameters - * error handling - * entry point handlers (application scripts): + +* bootstrap and initialization parameters +* error handling +* entry point handlers (application scripts): * HTTP -- the web-application entry point for serving pages of Storefront, Admin, etc * Static Resource -- for retrieving and serving static content (CSS, JavaScript, images) * Cron -- for launching cron jobs - * Object manager, filesystem components (inheritors specific to Magento application) - * Caching, cache types - * Language packages, dictionaries - * DB connection configuration and pool - * Request dispatching, routing, front controller - * Services for view layer - * Application areas +* Object manager, filesystem components (inheritors specific to Magento application) +* Caching, cache types +* Language packages, dictionaries +* DB connection configuration and pool +* Request dispatching, routing, front controller +* Services for view layer +* Application areas diff --git a/lib/internal/Magento/Framework/App/Request/Http.php b/lib/internal/Magento/Framework/App/Request/Http.php index c03b56c7eacb2..2535b499f8a6f 100644 --- a/lib/internal/Magento/Framework/App/Request/Http.php +++ b/lib/internal/Magento/Framework/App/Request/Http.php @@ -7,11 +7,13 @@ namespace Magento\Framework\App\Request; +use Laminas\Stdlib\Parameters; use Magento\Framework\App\HttpRequestInterface; use Magento\Framework\App\RequestContentInterface; use Magento\Framework\App\RequestSafetyInterface; use Magento\Framework\App\Route\ConfigInterface; use Magento\Framework\HTTP\PhpEnvironment\Request; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; use Magento\Framework\Stdlib\StringUtils; @@ -22,7 +24,11 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api */ -class Http extends Request implements RequestContentInterface, RequestSafetyInterface, HttpRequestInterface +class Http extends Request implements + RequestContentInterface, + RequestSafetyInterface, + HttpRequestInterface, + ResetAfterRequestInterface { /**#@+ * HTTP Ports @@ -423,4 +429,35 @@ public function isSafeMethod() } return $this->isSafeMethod; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->setEnv(new Parameters($_ENV)); + $this->serverParams = new Parameters($_SERVER); + $this->setQuery(new Parameters([])); + $this->setPost(new Parameters([])); + $this->setFiles(new Parameters([])); + $this->module = null; + $this->controller= null; + $this->action = null; + $this->pathInfo = ''; + $this->requestString = ''; + $this->params = []; + $this->aliases = []; + $this->dispatched = false; + $this->forwarded = null; + $this->baseUrl = null; + $this->basePath = null; + $this->requestUri = null; + $this->method = 'GET'; + $this->allowCustomMethods = true; + $this->uri = null; + $this->headers = null; + $this->metadata = []; + $this->content = ''; + $this->distroBaseUrl = null; + } } diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index f572533ff8db6..31dd43c83e461 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -11,6 +11,7 @@ use Magento\Framework\App\ResourceConnection\ConfigInterface as ResourceConfigInterface; use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Application provides ability to configure multiple connections to persistent storage. @@ -20,7 +21,7 @@ * @api * @since 100.0.2 */ -class ResourceConnection +class ResourceConnection implements ResetAfterRequestInterface { public const AUTO_UPDATE_ONCE = 0; public const AUTO_UPDATE_NEVER = -1; @@ -39,7 +40,7 @@ class ResourceConnection * * @var array */ - protected $mappedTableNames; + protected $mappedTableNames = []; /** * Resource config. @@ -83,6 +84,19 @@ public function __construct( $this->tablePrefix = $tablePrefix ?: null; } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->mappedTableNames = []; + foreach ($this->connections as $connection) { + if ($connection instanceof ResetAfterRequestInterface) { + $connection->_resetState(); + } + } + } + /** * Retrieve connection to resource specified by $resourceName. * diff --git a/lib/internal/Magento/Framework/App/Response/File.php b/lib/internal/Magento/Framework/App/Response/File.php new file mode 100644 index 0000000000000..f4c9b8486b069 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Response/File.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\App\Response; + +use InvalidArgumentException; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\PageCache\NotCacheableInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File\Mime; +use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Stdlib\DateTime; + +/** + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class File extends Http implements NotCacheableInterface +{ + private const DEFAULT_RAW_CONTENT_TYPE = 'application/octet-stream'; + + /** + * @var Http + */ + private Http $response; + + /** + * @var Filesystem + */ + private Filesystem $filesystem; + + /** + * @var Mime + */ + private Mime $mime; + + /** + * @var array + */ + private array $options = [ + 'directoryCode' => DirectoryList::ROOT, + 'filePath' => null, + // File name to send to the client + 'fileName' => null, + 'contentType' => null, + 'contentLength' => null, + // Whether to remove the file after it is sent to the client + 'remove' => false, + // Whether to send the file as attachment + 'attachment' => true + ]; + + /** + * @param HttpRequest $request + * @param CookieManagerInterface $cookieManager + * @param CookieMetadataFactory $cookieMetadataFactory + * @param Context $context + * @param DateTime $dateTime + * @param ConfigInterface $sessionConfig + * @param Http $response + * @param Filesystem $filesystem + * @param Mime $mime + * @param array $options + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + HttpRequest $request, + CookieManagerInterface $cookieManager, + CookieMetadataFactory $cookieMetadataFactory, + Context $context, + DateTime $dateTime, + ConfigInterface $sessionConfig, + Http $response, + Filesystem $filesystem, + Mime $mime, + array $options = [] + ) { + parent::__construct($request, $cookieManager, $cookieMetadataFactory, $context, $dateTime, $sessionConfig); + $this->response = $response; + $this->filesystem = $filesystem; + $this->mime = $mime; + $this->options = array_merge($this->options, $options); + if (!isset($this->options['filePath'])) { + if (!isset($this->options['fileName'])) { + throw new InvalidArgumentException("File name is required."); + } + $this->options['contentType'] ??= self::DEFAULT_RAW_CONTENT_TYPE; + } + } + + /** + * @inheritDoc + */ + public function sendResponse() + { + $dir = $this->filesystem->getDirectoryRead($this->options['directoryCode']); + $forceHeaders = true; + if (isset($this->options['filePath'])) { + if (!$dir->isExist($this->options['filePath'])) { + throw new InvalidArgumentException("File '{$this->options['filePath']}' does not exists."); + } + $filePath = $this->options['filePath']; + $this->options['contentType'] ??= $dir->stat($filePath)['mimeType'] + ?? $this->mime->getMimeType($dir->getAbsolutePath($filePath)); + $this->options['contentLength'] ??= $dir->stat($filePath)['size']; + $this->options['fileName'] ??= basename($filePath); + } else { + $this->options['contentLength'] = mb_strlen((string) $this->response->getContent(), '8bit'); + $forceHeaders = false; + } + + $this->response->setHttpResponseCode(200); + if ($this->options['attachment']) { + $this->response->setHeader( + 'Content-Disposition', + 'attachment; filename="' . $this->options['fileName'] . '"', + $forceHeaders + ); + } + $this->response->setHeader('Content-Type', $this->options['contentType'], $forceHeaders) + ->setHeader('Content-Length', $this->options['contentLength'], $forceHeaders) + ->setHeader('Pragma', 'public', $forceHeaders) + ->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', $forceHeaders) + ->setHeader('Last-Modified', date('r'), $forceHeaders); + + if (isset($this->options['filePath'])) { + $this->response->sendHeaders(); + if (!$this->request->isHead()) { + $this->sendFileContent(); + $this->afterFileIsSent(); + } + } else { + $this->response->sendResponse(); + } + return $this; + } + + /** + * @inheritDoc + */ + public function setHeader($name, $value, $replace = false) + { + $this->response->setHeader($name, $value, $replace); + return $this; + } + + /** + * @inheritDoc + */ + public function getHeader($name) + { + return $this->response->getHeader($name); + } + + /** + * @inheritDoc + */ + public function clearHeader($name) + { + $this->response->clearHeader($name); + return $this; + } + + /** + * @inheritDoc + */ + public function setBody($value) + { + $this->response->setBody($value); + return $this; + } + + /** + * @inheritDoc + */ + public function appendBody($value) + { + $this->response->appendBody($value); + return $this; + } + + /** + * @inheritDoc + */ + public function getContent() + { + return $this->response->getContent(); + } + + /** + * @inheritDoc + */ + public function setContent($value) + { + $this->response->setContent($value); + return $this; + } + + /** + * Sends file content to the client + * + * @return void + * @throws FileSystemException + */ + private function sendFileContent(): void + { + $dir = $this->filesystem->getDirectoryRead($this->options['directoryCode']); + $stream = $dir->openFile($this->options['filePath'], 'r'); + while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput + echo $stream->read(1024); + } + $stream->close(); + } + + /** + * Callback after file is sent to the client + * + * @return void + * @throws FileSystemException + */ + private function afterFileIsSent(): void + { + $this->response->clearBody(); + if ($this->options['remove']) { + $dir = $this->filesystem->getDirectoryWrite($this->options['directoryCode']); + $dir->delete($this->options['filePath']); + } + } +} diff --git a/lib/internal/Magento/Framework/App/Response/Http.php b/lib/internal/Magento/Framework/App/Response/Http.php index cb36408e9c925..feabc97705bd9 100644 --- a/lib/internal/Magento/Framework/App/Response/Http.php +++ b/lib/internal/Magento/Framework/App/Response/Http.php @@ -21,6 +21,7 @@ * @api * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ +#[\AllowDynamicProperties] class Http extends \Magento\Framework\HTTP\PhpEnvironment\Response { /** Cookie to store page vary string */ diff --git a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php index 02ef8e8123d06..232120ff75dd9 100644 --- a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php +++ b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php @@ -8,6 +8,9 @@ namespace Magento\Framework\App\Response\Http; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Filesystem; /** * Class FileFactory serves to declare file content in response for download. @@ -17,6 +20,8 @@ class FileFactory { /** + * @deprecared + * @see $fileResponseFactory * @var \Magento\Framework\App\ResponseInterface */ protected $_response; @@ -27,15 +32,24 @@ class FileFactory protected $_filesystem; /** - * @param \Magento\Framework\App\ResponseInterface $response - * @param \Magento\Framework\Filesystem $filesystem + * @var \Magento\Framework\App\Response\FileFactory + */ + private $fileResponseFactory; + + /** + * @param ResponseInterface $response + * @param Filesystem $filesystem + * @param \Magento\Framework\App\Response\FileFactory|null $fileResponseFactory */ public function __construct( \Magento\Framework\App\ResponseInterface $response, - \Magento\Framework\Filesystem $filesystem + \Magento\Framework\Filesystem $filesystem, + ?\Magento\Framework\App\Response\FileFactory $fileResponseFactory = null ) { $this->_response = $response; $this->_filesystem = $filesystem; + $this->fileResponseFactory = $fileResponseFactory + ?? ObjectManager::getInstance()->get(\Magento\Framework\App\Response\FileFactory::class); } /** @@ -79,38 +93,23 @@ public function create( $contentLength = $dir->stat($file)['size']; } } - $this->_response->setHttpResponseCode(200) - ->setHeader('Pragma', 'public', true) - ->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true) - ->setHeader('Content-type', $contentType, true) - ->setHeader('Content-Length', $contentLength === null ? strlen((string)$fileContent) : $contentLength, true) - ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"', true) - ->setHeader('Last-Modified', date('r'), true); if ($content !== null) { - $this->_response->sendHeaders(); - if ($isFile) { - $stream = $dir->openFile($file, 'r'); - while (!$stream->eof()) { - // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $stream->read(1024); - } - } else { + if (!$isFile) { $dir->writeFile($fileName, $fileContent); $file = $fileName; - $stream = $dir->openFile($fileName, 'r'); - while (!$stream->eof()) { - // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $stream->read(1024); - } - } - $stream->close(); - flush(); - if (!empty($content['rm'])) { - $dir->delete($file); } } - return $this->_response; + return $this->fileResponseFactory->create([ + 'options' => [ + 'filePath' => $file, + 'fileName' => $fileName, + 'contentType' => $contentType, + 'contentLength' => $contentLength, + 'directoryCode' => $baseDir, + 'remove' => is_array($content) && !empty($content['rm']) + ] + ]); } /** diff --git a/lib/internal/Magento/Framework/App/State.php b/lib/internal/Magento/Framework/App/State.php index bc2b85b37442b..5956c7063c892 100644 --- a/lib/internal/Magento/Framework/App/State.php +++ b/lib/internal/Magento/Framework/App/State.php @@ -19,7 +19,7 @@ class State /** * Application run code */ - const PARAM_MODE = 'MAGE_MODE'; + public const PARAM_MODE = 'MAGE_MODE'; /** * Application mode @@ -50,8 +50,6 @@ class State protected $_configScope; /** - * Area code - * * @var string */ protected $_areaCode; @@ -68,16 +66,14 @@ class State */ private $areaList; - /**#@+ + /** * Application modes */ - const MODE_DEVELOPER = 'developer'; + public const MODE_DEVELOPER = 'developer'; - const MODE_PRODUCTION = 'production'; + public const MODE_PRODUCTION = 'production'; - const MODE_DEFAULT = 'default'; - - /**#@-*/ + public const MODE_DEFAULT = 'default'; /** * @param \Magento\Framework\Config\ScopeInterface $configScope @@ -185,13 +181,11 @@ public function emulateAreaCode($areaCode, $callback, $params = []) $this->_isAreaCodeEmulated = true; try { $result = call_user_func_array($callback, $params); - } catch (\Exception $e) { + } finally { $this->_areaCode = $currentArea; $this->_isAreaCodeEmulated = false; - throw $e; } - $this->_areaCode = $currentArea; - $this->_isAreaCodeEmulated = false; + return $result; } @@ -221,6 +215,7 @@ private function checkAreaCode($areaCode) * * @return AreaList * @deprecated 101.0.0 + * @see Nothing */ private function getAreaListInstance() { diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php index 42b4d7866c23b..78e9d27c7ab21 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php @@ -50,6 +50,14 @@ class DeploymentConfigTest extends TestCase 'test_override' => 'overridden', ]; + /** + * @var array + */ + private static $flattenedFixtureSecond + = [ + 'test_override' => 'overridden2' + ]; + /** * @var array */ @@ -112,6 +120,25 @@ public function testGetters(): void $this->assertSame('overridden', $this->deploymentConfig->get('test_override')); } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + public function testGettersReloadConfig(): void + { + $this->readerMock->expects($this->any())->method('load')->willReturn(self::$flattenedFixtureSecond); + $this->deploymentConfig = new DeploymentConfig( + $this->readerMock, + ['test_override' => 'overridden2'] + ); + $this->assertNull($this->deploymentConfig->get('invalid_key')); + $this->assertNull($this->deploymentConfig->getConfigData('invalid_key')); + putenv('MAGENTO_DC_A=abc'); + $this->assertSame('abc', $this->deploymentConfig->get('a')); + $this->assertSame('overridden2', $this->deploymentConfig->get('test_override')); + } + /** * @return void * @throws FileSystemException @@ -149,7 +176,7 @@ public function testNotAvailable(): void */ public function testNotAvailableThenAvailable(): void { - $this->readerMock->expects($this->exactly(2))->method('load')->willReturn(['Test']); + $this->readerMock->expects($this->exactly(1))->method('load')->willReturn(['Test']); $object = new DeploymentConfig($this->readerMock); $this->assertFalse($object->isAvailable()); $this->assertFalse($object->isAvailable()); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php b/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php index 164286728f3b7..ea6b4cbf9b199 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php @@ -14,6 +14,7 @@ use Magento\Framework\App\PageCache\Cache; use Magento\Framework\App\PageCache\Identifier; use Magento\Framework\App\PageCache\Kernel; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\Request\Http; use Magento\Framework\App\Response\HttpFactory; use Magento\Framework\Serialize\SerializerInterface; @@ -328,4 +329,28 @@ public function processNotSaveCacheProvider(): array ['public, max-age=100, s-maxage=100', 200, false, true] ]; } + + public function testProcessNotSaveCacheForNotCacheableResponse(): void + { + $header = CacheControl::fromString("Cache-Control: public, max-age=100, s-maxage=100"); + $notCacheableResponse = $this->getMockBuilder(\Magento\Framework\App\Response\File::class) + ->disableOriginalConstructor() + ->getMock(); + + $notCacheableResponse->expects($this->once()) + ->method('getHeader') + ->with('Cache-Control') + ->willReturn($header); + $notCacheableResponse->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(200); + $notCacheableResponse->expects($this->once()) + ->method('setNoCacheHeaders'); + $this->requestMock + ->expects($this->any())->method('isGet') + ->willReturn(true); + $this->fullPageCacheMock->expects($this->never()) + ->method('save'); + $this->kernel->process($notCacheableResponse); + } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php new file mode 100644 index 0000000000000..0fdd80b79567e --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php @@ -0,0 +1,409 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\App\Test\Unit\Response; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Request\Http as RequestHttp; +use Magento\Framework\App\Response\File; +use Magento\Framework\App\Response\Http; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File\Mime; +use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Stdlib\DateTime; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FileTest extends TestCase +{ + /** + * @var RequestHttp|MockObject + */ + private $requestMock; + /** + * @var CookieMetadataFactory|MockObject + */ + private $cookieMetadataFactoryMock; + /** + * @var CookieManagerInterface|MockObject + */ + private $cookieManagerMock; + /** + * @var Context|MockObject + */ + private $contextMock; + /** + * @var DateTime|MockObject + */ + private $dateTimeMock; + /** + * @var ConfigInterface|MockObject + */ + private $sessionConfigMock; + /** + * @var Filesystem|MockObject + */ + private $filesystemMock; + /** + * @var Mime|MockObject + */ + private $mimeMock; + /** + * @var Http|MockObject + */ + private $responseMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->requestMock = $this->getMockBuilder(RequestHttp::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadataFactoryMock = $this->getMockBuilder(CookieMetadataFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieManagerMock = $this->getMockForAbstractClass(CookieManagerInterface::class); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionConfigMock = $this->getMockBuilder(ConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $this->mimeMock = $this->getMockBuilder(Mime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testSendResponseWithMissingFilePath(): void + { + $options = []; + $this->expectExceptionMessage('File name is required.'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithFileThatDoesNotExist(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $directory->expects($this->once()) + ->method('isExist') + ->willReturn(false); + $this->expectExceptionMessage("File 'path/to/file.pdf' does not exists."); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithFilePath(): void + { + $fileSize = 1024; + $filePath = 'path/to/file.pdf'; + $fileAbsolutePath = 'path/to/root/path/to/file.pdf'; + $fileName = 'file.pdf'; + $fileMimetype = 'application/pdf'; + $stat = [ + 'size' => $fileSize + ]; + $options = [ + 'filePath' => $filePath + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $directory->expects($this->once()) + ->method('isExist') + ->with($filePath) + ->willReturn(true); + $directory->expects($this->once()) + ->method('getAbsolutePath') + ->with($filePath) + ->willReturn($fileAbsolutePath); + $directory->expects($this->exactly(2)) + ->method('stat') + ->with($filePath) + ->willReturn($stat); + $writeDirectory = $this->getMockForAbstractClass(WriteInterface::class); + $writeDirectory->expects($this->never()) + ->method('delete') + ->with($filePath); + $stream = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\WriteInterface::class); + $directory->expects($this->once()) + ->method('openFile') + ->with($filePath) + ->willReturn($stream); + $stream->expects($this->once()) + ->method('eof') + ->willReturn(true); + $stream->expects($this->once()) + ->method('close'); + $this->filesystemMock->expects($this->exactly(2)) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $this->filesystemMock->expects($this->never()) + ->method('getDirectoryWrite') + ->with(DirectoryList::ROOT) + ->willReturn($writeDirectory); + $this->mimeMock->expects($this->once()) + ->method('getMimeType') + ->willReturn($fileMimetype); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(200); + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', true], + ['Content-Type', $fileMimetype, true], + ['Content-Length', $fileSize, true], + ['Pragma', 'public', true], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + true + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithRemoveOption(): void + { + $fileSize = 1024; + $filePath = 'path/to/file.pdf'; + $fileAbsolutePath = 'path/to/root/path/to/file.pdf'; + $fileName = 'file.pdf'; + $fileMimetype = 'application/pdf'; + $stat = [ + 'size' => $fileSize + ]; + $options = [ + 'filePath' => $filePath, + 'remove' => true + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $directory->expects($this->once()) + ->method('isExist') + ->with($filePath) + ->willReturn(true); + $directory->expects($this->once()) + ->method('getAbsolutePath') + ->with($filePath) + ->willReturn($fileAbsolutePath); + $directory->expects($this->exactly(2)) + ->method('stat') + ->with($filePath) + ->willReturn($stat); + $writeDirectory = $this->getMockForAbstractClass(WriteInterface::class); + $writeDirectory->expects($this->once()) + ->method('delete') + ->with($filePath); + $stream = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\WriteInterface::class); + $directory->expects($this->once()) + ->method('openFile') + ->with($filePath) + ->willReturn($stream); + $stream->expects($this->once()) + ->method('eof') + ->willReturn(true); + $stream->expects($this->once()) + ->method('close'); + $this->filesystemMock->expects($this->exactly(2)) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::ROOT) + ->willReturn($writeDirectory); + $this->mimeMock->expects($this->once()) + ->method('getMimeType') + ->willReturn($fileMimetype); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(200); + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', true], + ['Content-Type', $fileMimetype, true], + ['Content-Length', $fileSize, true], + ['Pragma', 'public', true], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + true + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithRawContent(): void + { + $fileMimetype = 'application/octet-stream'; + $fileSize = 18; + $fileName = 'file.pdf'; + $options = [ + 'fileName' => $fileName, + ]; + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', false], + ['Content-Type', $fileMimetype, false], + ['Content-Length', $fileSize, false], + ['Pragma', 'public', false], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', false], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + false + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn('Bienvenue à Paris'); + $this->getModel($options)->sendResponse(); + } + + public function testSetHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 1024, true) + ->willReturnSelf(); + $this->assertSame($model, $model->setHeader('Content-Type', 1024, true)); + } + + public function testGetHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn(2048); + $this->assertEquals(2048, $model->getHeader('Content-Type')); + } + + public function testClearHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('clearHeader') + ->with('Content-Type') + ->willReturnSelf(); + $this->assertSame($model, $model->clearHeader('Content-Type')); + } + + public function testSetBody(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setBody') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->setBody('Hello World')); + } + + public function testAppendBody(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('appendBody') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->appendBody('Hello World')); + } + + public function testGetContent(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn('Hello World'); + $this->assertEquals('Hello World', $model->getContent()); + } + + public function testSetContent(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setContent') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->setContent('Hello World')); + } + + private function getModel(array $options = []): File + { + return new File( + $this->requestMock, + $this->cookieManagerMock, + $this->cookieMetadataFactoryMock, + $this->contextMock, + $this->dateTimeMock, + $this->sessionConfigMock, + $this->responseMock, + $this->filesystemMock, + $this->mimeMock, + $options + ); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php index 7fbbedd7f9136..5036ac3992969 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php @@ -7,12 +7,13 @@ namespace Magento\Framework\App\Test\Unit\Response\Http; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Directory\WriteInterface as DirectoryWriteInterface; -use Magento\Framework\Filesystem\File\WriteInterface as FileWriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -39,6 +40,16 @@ class FileFactoryTest extends TestCase */ protected $dirMock; + /** + * @var \Magento\Framework\App\Response\FileFactory|MockObject + */ + private $fileResponseFactory; + + /** + * @var FileFactory + */ + private $model; + /** * @inheritDoc */ @@ -75,6 +86,8 @@ protected function setUp(): void Http::class, ['setHeader', 'sendHeaders', 'setHttpResponseCode', 'clearBody', 'setBody', '__wakeup'] ); + $this->fileResponseFactory = $this->createMock(\Magento\Framework\App\Response\FileFactory::class); + $this->model = new FileFactory($this->responseMock, $this->fileSystemMock, $this->fileResponseFactory); } /** @@ -83,7 +96,7 @@ protected function setUp(): void public function testCreateIfContentDoesntHaveRequiredKeys(): void { $this->expectException('InvalidArgumentException'); - $this->getModel()->create('fileName', []); + $this->model->create('fileName', []); } /** @@ -106,7 +119,7 @@ public function testCreateIfFileNotExist(): void )->method( 'setHttpResponseCode' )->willReturnSelf(); - $this->getModel()->create('fileName', $content); + $this->model->create('fileName', $content); } /** @@ -116,38 +129,29 @@ public function testCreateArrayContent(): void { $file = 'some_file'; $content = ['type' => 'filename', 'value' => $file]; - + $fileSize = 100; + + $responseMock = $this->getMockForAbstractClass(ResponseInterface::class); + $this->fileResponseFactory->expects($this->once()) + ->method('create') + ->with([ + 'options' => [ + 'filePath' => $file, + 'fileName' => 'fileName', + 'contentType' => 'application/octet-stream', + 'contentLength' => $fileSize, + 'directoryCode' => DirectoryList::ROOT, + 'remove' => false + ] + ]) + ->willReturn($responseMock); $this->dirMock->expects($this->once()) ->method('isFile') ->willReturn(true); $this->dirMock->expects($this->once()) ->method('stat') - ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); - - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); - $this->dirMock->expects($this->never()) - ->method('delete') - ->willReturn($streamMock); - $streamMock - ->method('eof') - ->willReturnOnConsecutiveCalls(false, true); - $streamMock->expects($this->once()) - ->method('read'); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', $content); + ->willReturn(['size' => $fileSize]); + $this->model->create('fileName', $content); } /** @@ -157,38 +161,35 @@ public function testCreateArrayContentRm(): void { $file = 'some_file'; $content = ['type' => 'filename', 'value' => $file, 'rm' => 1]; + $fileSize = 100; $this->dirMock->expects($this->once()) ->method('isFile') ->willReturn(true); $this->dirMock->expects($this->once()) ->method('stat') - ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); - - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->willReturn(['size' => $fileSize]); + $responseMock = $this->getMockForAbstractClass(ResponseInterface::class); + $this->fileResponseFactory->expects($this->once()) + ->method('create') + ->with([ + 'options' => [ + 'filePath' => $file, + 'fileName' => 'fileName', + 'contentType' => 'application/octet-stream', + 'contentLength' => $fileSize, + 'directoryCode' => DirectoryList::ROOT, + 'remove' => true + ] + ]) + ->willReturn($responseMock); $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); + ->method('isFile') + ->willReturn(true); $this->dirMock->expects($this->once()) - ->method('delete') - ->willReturn($streamMock); - $streamMock - ->method('eof') - ->willReturnOnConsecutiveCalls(false, true); - $streamMock->expects($this->once()) - ->method('read'); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', $content); + ->method('stat') + ->willReturn(['size' => $fileSize]); + $this->model->create('fileName', $content); } /** @@ -202,62 +203,9 @@ public function testCreateStringContent(): void $this->dirMock->expects($this->never()) ->method('stat') ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); $this->dirMock->expects($this->once()) ->method('writeFile') ->with('fileName', 'content', 'w+'); - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); - $streamMock->expects($this->once()) - ->method('eof') - ->willReturn(true); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', 'content'); - } - - /** - * Get model. - * - * @return FileFactory|object - */ - private function getModel() - { - return $this->objectManager->getObject( - FileFactory::class, - [ - 'response' => $this->responseMock, - 'filesystem' => $this->fileSystemMock - ] - ); - } - - /** - * Get model mock. - * - * @return FileFactory|MockObject - */ - private function getModelMock(): MockObject - { - $modelMock = $this->getMockBuilder(FileFactory::class) - ->onlyMethods([]) - ->setConstructorArgs( - [ - 'response' => $this->responseMock, - 'filesystem' => $this->fileSystemMock - ] - ) - ->getMock(); - return $modelMock; + $this->model->create('fileName', 'content'); } } 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 5f4af5e8ae519..36f1ddaf5beb7 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php @@ -112,7 +112,6 @@ protected function setUp(): void 'sessionConfig' => $this->sessionConfigMock ] ); - $this->model->headersSentThrowsException = false; $this->model->setHeader('Name', 'Value'); } diff --git a/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php b/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php index 7b27f84a9efd1..49186d22542dd 100644 --- a/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php +++ b/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php @@ -37,6 +37,7 @@ public function __construct($testCase, array $options = []) /** * Collect all failed assertions and fail test in case such list is not empty. + * * Incomplete and skipped test results are aggregated as well. * * @param callable $callback @@ -71,6 +72,8 @@ public function __invoke(callable $callback, array $dataSource) } /** + * Prepare Message + * * @param \Exception $exception * @param string $dataSetName * @param mixed $dataSet @@ -127,7 +130,7 @@ protected function processResults(array $results, $passed) $results[\PHPUnit\Framework\SkippedTestError::class] ); if ($results[\PHPUnit\Framework\IncompleteTestError::class]) { - $this->_testCase->markTestIncomplete($message); + $this->_testCase->markTestSkipped($message); } elseif ($results[\PHPUnit\Framework\SkippedTestError::class]) { $this->_testCase->markTestSkipped($message); } diff --git a/lib/internal/Magento/Framework/Archive/README.md b/lib/internal/Magento/Framework/Archive/README.md index 4fa38a201db6e..197089214b0a7 100644 --- a/lib/internal/Magento/Framework/Archive/README.md +++ b/lib/internal/Magento/Framework/Archive/README.md @@ -1,4 +1,5 @@ Archive library provides functionalities for archiving files including following formats: + * tar * gz -* bzip2 \ No newline at end of file +* bzip2 diff --git a/lib/internal/Magento/Framework/Async/README.md b/lib/internal/Magento/Framework/Async/README.md index f71598637601c..ac4b9772a562c 100644 --- a/lib/internal/Magento/Framework/Async/README.md +++ b/lib/internal/Magento/Framework/Async/README.md @@ -1 +1 @@ -Async library provides classes to work with asynchronous/deferred operations, for instance sending an HTTP request. \ No newline at end of file +Async library provides classes to work with asynchronous/deferred operations, for instance sending an HTTP request. diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php b/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php index b58ad53dd139b..1409fba14c5f3 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php @@ -19,6 +19,13 @@ class File extends \SplFileObject */ protected $_currentStatement = ''; + /** + * Store current statement delimiter. + * + * @var string + */ + private string $statementDelimiter = ';'; + /** * Return current sql statement * @@ -41,15 +48,35 @@ public function next() $this->_currentStatement = ''; while (!$this->eof()) { $line = $this->fgets(); - if (strlen(trim($line))) { - $this->_currentStatement .= $line; - if ($this->_isLineLastInCommand($line)) { + $trimmedLine = trim($line); + if (!empty($trimmedLine) && !$this->isDelimiterChanged($trimmedLine)) { + $statementFinalLine = '/(?<statement>.*)' . preg_quote($this->statementDelimiter, '/') . '$/'; + if (preg_match($statementFinalLine, $trimmedLine, $matches)) { + $this->_currentStatement .= $matches['statement']; break; + } else { + $this->_currentStatement .= $line; } } } } + /** + * Check whether statement delimiter has been changed. + * + * @param string $line + * @return bool + */ + private function isDelimiterChanged(string $line): bool + { + if (preg_match('/^delimiter\s+(?<delimiter>.+)$/i', $line, $matches)) { + $this->statementDelimiter = $matches['delimiter']; + return true; + } + + return false; + } + /** * Return to first statement * @@ -72,26 +99,4 @@ protected function _isComment($line) { return $line[0] == '#' || ($line && substr($line, 0, 2) == '--'); } - - /** - * Check is line a last in sql command - * - * @param string $line - * @return bool - */ - protected function _isLineLastInCommand($line) - { - $cleanLine = trim($line); - $lineLength = strlen($cleanLine); - - $returnResult = false; - if ($lineLength > 0) { - $lastSymbolIndex = $lineLength - 1; - if ($cleanLine[$lastSymbolIndex] == ';') { - $returnResult = true; - } - } - - return $returnResult; - } } diff --git a/lib/internal/Magento/Framework/Backup/README.md b/lib/internal/Magento/Framework/Backup/README.md index e3785a3025e33..4cddf00d77b9b 100644 --- a/lib/internal/Magento/Framework/Backup/README.md +++ b/lib/internal/Magento/Framework/Backup/README.md @@ -1 +1 @@ -The Backup library provides functions to create and rollback backup types such as database, filesystem and media. It also provides an archiving facility. \ No newline at end of file +The Backup library provides functions to create and rollback backup types such as database, filesystem and media. It also provides an archiving facility. diff --git a/lib/internal/Magento/Framework/Bulk/README.md b/lib/internal/Magento/Framework/Bulk/README.md index 8ddbc686147ff..d0ee4069093f3 100644 --- a/lib/internal/Magento/Framework/Bulk/README.md +++ b/lib/internal/Magento/Framework/Bulk/README.md @@ -1 +1 @@ - This component is designed to provide Bulk Operations Framework. \ No newline at end of file + This component is designed to provide Bulk Operations Framework. diff --git a/lib/internal/Magento/Framework/Cache/Backend/Redis.php b/lib/internal/Magento/Framework/Cache/Backend/Redis.php index 565777d68ff63..c02878ef79506 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Redis.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Redis.php @@ -72,6 +72,7 @@ public function load($id, $doNotTestCacheValidity = false) */ public function save($data, $id, $tags = [], $specificLifetime = false) { + // @todo add special handling of MAGE tag, save clenup try { $result = parent::save($data, $id, $tags, $specificLifetime); } catch (\Throwable $exception) { @@ -94,4 +95,15 @@ public function remove($id) return $result; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magento/Framework/Cache/Core.php index 1c1bab29b75af..2a97bc0f49c7a 100644 --- a/lib/internal/Magento/Framework/Cache/Core.php +++ b/lib/internal/Magento/Framework/Cache/Core.php @@ -5,6 +5,10 @@ */ namespace Magento\Framework\Cache; +use Magento\Framework\Cache\Backend\Redis; +use Zend_Cache; +use Zend_Cache_Exception; + class Core extends \Zend_Cache_Core { /** @@ -53,17 +57,7 @@ protected function _tags($tags) } /** - * Save some data in a cache - * - * @param mixed $data Data to put in cache (can be another type than string if - * automatic_serialization is on) - * @param null|string $cacheId Cache id (if not set, the last cache id will be used) - * @param string[] $tags Cache tags - * @param bool|int $specificLifetime If != false, set a specific lifetime for this cache record - * (null => infinite lifetime) - * @param int $priority integer between 0 (very low priority) and 10 (maximum priority) used by - * some particular backends - * @return bool True if no problem + * @inheritDoc */ public function save($data, $cacheId = null, $tags = [], $specificLifetime = false, $priority = 8) { @@ -126,6 +120,34 @@ public function getIdsNotMatchingTags($tags = []) return parent::getIdsNotMatchingTags($tags); } + /** + * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) + * + * Throw an exception if a problem is found + * + * @param string $string Cache id or tag + * @throws Zend_Cache_Exception + * @return void + */ + protected function _validateIdOrTag($string) + { + if ($this->_backend instanceof Redis) { + if (!is_string($string)) { + Zend_Cache::throwException('Invalid id or tag : must be a string'); + } + if (substr($string, 0, 9) == 'internal-') { + Zend_Cache::throwException('"internal-*" ids or tags are reserved'); + } + if (!preg_match('~^[a-zA-Z0-9_{}]+$~D', $string)) { + Zend_Cache::throwException("Invalid id or tag '$string' : must use only [a-zA-Z0-9_{}]"); + } + + return; + } + + parent::_validateIdOrTag($string); + } + /** * Set the backend * @@ -177,4 +199,15 @@ protected function _decorateBackend(\Zend_Cache_Backend $backendObject) return $backendObject; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php index 785ca43ec9355..737105585e2e5 100644 --- a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php +++ b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php @@ -50,7 +50,7 @@ protected function _getFrontend() } /** - * {@inheritdoc} + * @inheritdoc */ public function test($identifier) { @@ -58,7 +58,7 @@ public function test($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function load($identifier) { @@ -66,9 +66,7 @@ public function load($identifier) } /** - * Enforce marking with a tag - * - * {@inheritdoc} + * @inheritDoc */ public function save($data, $identifier, array $tags = [], $lifeTime = null) { @@ -76,7 +74,7 @@ public function save($data, $identifier, array $tags = [], $lifeTime = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function remove($identifier) { @@ -84,7 +82,7 @@ public function remove($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) { @@ -92,7 +90,7 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getBackend() { @@ -100,10 +98,21 @@ public function getBackend() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLowLevelFrontend() { return $this->_getFrontend()->getLowLevelFrontend(); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php index 503fb1a569e2c..deb7bd5ee3480 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php @@ -11,8 +11,13 @@ namespace Magento\Framework\Cache\Test\Unit; use Magento\Framework\Cache\Backend\Decorator\AbstractDecorator; +use Magento\Framework\Cache\Backend\Redis; use Magento\Framework\Cache\Core; +use Magento\Framework\Cache\Frontend\Adapter\Zend; +use Magento\Framework\Cache\Frontend\Decorator\Bare; +use Magento\Framework\Cache\FrontendInterface; use PHPUnit\Framework\TestCase; +use Zend_Cache_Exception; class CoreTest extends TestCase { @@ -199,4 +204,33 @@ public function testGetIdsNotMatchingTags() $result = $frontend->getIdsNotMatchingTags($tags); $this->assertEquals($ids, $result); } + + public function testLoadAllowsToUseCurlyBracketsInPrefixOnRedisBackend() + { + $id = 'abc'; + + $mockBackend = $this->createMock(Redis::class); + $core = new Core([ + 'cache_id_prefix' => '{prefix}_' + ]); + $core->setBackend($mockBackend); + + $core->load($id); + $this->assertNull(null); + } + + public function testLoadNotAllowsToUseCurlyBracketsInPrefixOnNonRedisBackend() + { + $id = 'abc'; + + $core = new Core([ + 'cache_id_prefix' => '{prefix}_' + ]); + $core->setBackend($this->_mockBackend); + + $this->expectException(Zend_Cache_Exception::class); + $this->expectExceptionMessage("Invalid id or tag '{prefix}_abc' : must use only [a-zA-Z0-9_]"); + + $core->load($id); + } } diff --git a/lib/internal/Magento/Framework/Code/README.md b/lib/internal/Magento/Framework/Code/README.md index 6868b68e8d881..c058607922093 100644 --- a/lib/internal/Magento/Framework/Code/README.md +++ b/lib/internal/Magento/Framework/Code/README.md @@ -1,6 +1,7 @@ # Code **Code** library provides functionalities for processing code, including the following: + * Generating service entities - factories, proxies and interceptors. * Minifying content * Class, arguments reader diff --git a/lib/internal/Magento/Framework/Code/Reader/ClassReader.php b/lib/internal/Magento/Framework/Code/Reader/ClassReader.php index 759168372fdcd..9bc0551b4d1dd 100644 --- a/lib/internal/Magento/Framework/Code/Reader/ClassReader.php +++ b/lib/internal/Magento/Framework/Code/Reader/ClassReader.php @@ -121,4 +121,15 @@ public function getParents($className) return $result; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Communication/README.md b/lib/internal/Magento/Framework/Communication/README.md index cf0dce084c5dc..105b0a8afec7b 100644 --- a/lib/internal/Magento/Framework/Communication/README.md +++ b/lib/internal/Magento/Framework/Communication/README.md @@ -1,2 +1,2 @@ This component provides capabilities for connection to remote systems using any available transport mechanisms. -Concrete transport implementations are provided by other components. \ No newline at end of file +Concrete transport implementations are provided by other components. diff --git a/lib/internal/Magento/Framework/Component/README.md b/lib/internal/Magento/Framework/Component/README.md index 6e99e472cb799..02df6996bc65b 100644 --- a/lib/internal/Magento/Framework/Component/README.md +++ b/lib/internal/Magento/Framework/Component/README.md @@ -2,20 +2,27 @@ **Component** library provides feature for components (modules/themes/languages/libraries) to load from any custom directory like vendor. + * Modules should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::MODULE, '<module name>', __DIR__); ``` + * Themes should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::THEME, '<theme name>', __DIR__); ``` + * Languages should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::LANGUAGE, '<language name>', __DIR__); ``` + * Libraries should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::LIBRARY, '<library name>', __DIR__); ``` - diff --git a/lib/internal/Magento/Framework/Composer/DependencyChecker.php b/lib/internal/Magento/Framework/Composer/DependencyChecker.php index 6084b574235e2..9f402e079c458 100644 --- a/lib/internal/Magento/Framework/Composer/DependencyChecker.php +++ b/lib/internal/Magento/Framework/Composer/DependencyChecker.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Composer; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -16,9 +17,9 @@ class DependencyChecker { /** - * @var Application + * @var ApplicationFactory */ - private $composerApp; + private $applicationFactory; /** * @var DirectoryList @@ -28,12 +29,12 @@ class DependencyChecker /** * Constructor * - * @param Application $composerApp + * @param ApplicationFactory $applicationFactory * @param DirectoryList $directoryList */ - public function __construct(Application $composerApp, DirectoryList $directoryList) + public function __construct(ApplicationFactory $applicationFactory, DirectoryList $directoryList) { - $this->composerApp = $composerApp; + $this->applicationFactory = $applicationFactory; $this->directoryList = $directoryList; } @@ -49,12 +50,13 @@ public function __construct(Application $composerApp, DirectoryList $directoryLi */ public function checkDependencies(array $packages, $excludeSelf = false) { - $this->composerApp->setAutoExit(false); + $app = $this->applicationFactory->create(); + $app->setAutoExit(false); $dependencies = []; foreach ($packages as $package) { $buffer = new BufferedOutput(); - $this->composerApp->resetComposer(); - $this->composerApp->run( + $app->resetComposer(); + $app->run( new ArrayInput( ['command' => 'depends', '--working-dir' => $this->directoryList->getRoot(), 'package' => $package] ), diff --git a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php index 1e4168ca5b628..d5c5b428a75f9 100644 --- a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php +++ b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php @@ -8,28 +8,53 @@ namespace Magento\Framework\Composer\Test\Unit; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Composer\DependencyChecker; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DependencyCheckerTest extends TestCase { + /** - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @var ApplicationFactory|MockObject */ - public function testCheckDependencies(): void + private ApplicationFactory $composerFactory; + + /** + * @var Application|MockObject + */ + private Application $composerApp; + + protected function setUp(): void { - $composerApp = $this->getMockBuilder(Application::class) + $this->composerFactory = $this->getMockBuilder(ApplicationFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->composerApp = $this->getMockBuilder(Application::class) ->setMethods(['setAutoExit', 'resetComposer', 'run','__destruct']) ->disableOriginalConstructor() ->getMock(); + $this->composerFactory->method('create')->willReturn($this->composerApp); + parent::setUp(); + } + + /** + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function testCheckDependencies(): void + { + $directoryList = $this->createMock(DirectoryList::class); $directoryList->expects($this->exactly(2))->method('getRoot'); - $composerApp->expects($this->once())->method('setAutoExit')->with(false); - $composerApp->expects($this->any())->method('__destruct'); + $this->composerApp->expects($this->once())->method('setAutoExit')->with(false); + $this->composerApp->expects($this->any())->method('__destruct'); - $composerApp + $this->composerApp ->method('run') ->willReturnOnConsecutiveCalls( $this->returnCallback( @@ -52,7 +77,7 @@ function ($input, $buffer) { ) ); - $dependencyChecker = new DependencyChecker($composerApp, $directoryList); + $dependencyChecker = new DependencyChecker($this->composerFactory, $directoryList); $expected = [ 'magento/package-a' => ['magento/package-b', 'magento/package-c'], 'magento/package-b' => ['magento/package-c', 'magento/package-d'], @@ -69,16 +94,12 @@ function ($input, $buffer) { */ public function testCheckDependenciesExcludeSelf(): void { - $composerApp = $this->getMockBuilder(Application::class) - ->setMethods(['setAutoExit', 'resetComposer', 'run','__destruct']) - ->disableOriginalConstructor() - ->getMock(); $directoryList = $this->createMock(DirectoryList::class); $directoryList->expects($this->exactly(3))->method('getRoot'); - $composerApp->expects($this->once())->method('setAutoExit')->with(false); - $composerApp->expects($this->any())->method('__destruct'); + $this->composerApp->expects($this->once())->method('setAutoExit')->with(false); + $this->composerApp->expects($this->any())->method('__destruct'); - $composerApp + $this->composerApp ->method('run') ->willReturnOnConsecutiveCalls( $this->returnCallback( @@ -109,7 +130,7 @@ function ($input, $buffer) { ) ); - $dependencyChecker = new DependencyChecker($composerApp, $directoryList); + $dependencyChecker = new DependencyChecker($this->composerFactory, $directoryList); $expected = [ 'magento/package-a' => [], 'magento/package-b' => ['magento/package-d'], diff --git a/lib/internal/Magento/Framework/Config/Data.php b/lib/internal/Magento/Framework/Config/Data.php index cc11b32c410ba..a847b7f45e2b5 100644 --- a/lib/internal/Magento/Framework/Config/Data.php +++ b/lib/internal/Magento/Framework/Config/Data.php @@ -39,8 +39,6 @@ class Data implements \Magento\Framework\Config\DataInterface protected $_cacheId; /** - * Cache tags - * * @var array */ protected $cacheTags = []; @@ -154,5 +152,21 @@ public function get($path = null, $default = null) public function reset() { $this->cache->remove($this->cacheId); + $this->_data = []; + $configData = $this->reader->read(); + if ($configData) { + $this->merge($configData); + } + } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; } } diff --git a/lib/internal/Magento/Framework/Console/README.md b/lib/internal/Magento/Framework/Console/README.md index 245b85a27d4ea..534436c2dc69d 100644 --- a/lib/internal/Magento/Framework/Console/README.md +++ b/lib/internal/Magento/Framework/Console/README.md @@ -4,7 +4,7 @@ This component contains Magento Cli and can be extended via DI configuration. For example we can introduce new command in module using di.xml: -``` +```xml <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> @@ -13,4 +13,3 @@ For example we can introduce new command in module using di.xml: </arguments> </type> ``` - diff --git a/lib/internal/Magento/Framework/Controller/README.md b/lib/internal/Magento/Framework/Controller/README.md index ec10676d52620..bf1649b0f4c29 100644 --- a/lib/internal/Magento/Framework/Controller/README.md +++ b/lib/internal/Magento/Framework/Controller/README.md @@ -4,4 +4,4 @@ * **Response** * Adapter for Zend Response class. Needed for DI -* **Router** * Route Factory \ No newline at end of file +* **Router** * Route Factory diff --git a/lib/internal/Magento/Framework/Convert/Excel.php b/lib/internal/Magento/Framework/Convert/Excel.php index e201978a8cb99..1f15340d1b9bd 100644 --- a/lib/internal/Magento/Framework/Convert/Excel.php +++ b/lib/internal/Magento/Framework/Convert/Excel.php @@ -150,7 +150,8 @@ protected function _getXmlRow($row, $useCallback) foreach ($row as $value) { $value = $this->escaper->escapeHtml($value); - $dataType = is_numeric($value) && $value[0] !== '+' && $value[0] !== '0' ? 'Number' : 'String'; + $dataType = is_numeric($value) && (is_string($value) && ctype_space($value[0]) === false) && + $value[0] !== '+' && $value[0] !== '0' ? 'Number' : 'String'; /** * Security enhancement for CSV data processing by Excel-like applications. diff --git a/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php b/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php index ac7b89ec8f09b..4d7e59fde800e 100644 --- a/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php +++ b/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php @@ -20,29 +20,37 @@ class ExcelTest extends TestCase { /** - * Test data + * Test excel data * * @var array */ private $_testData = [ [ 'ID', 'Name', 'Email', 'Group', 'Telephone', '+Telephone', 'ZIP', '0ZIP', 'Country', 'State/Province', - 'Symbol=', 'Symbol-', 'Symbol+' + 'Symbol=', 'Symbol-', 'Symbol+', 'NumberWithSpace', 'NumberWithTabulation' ], [ 1, 'Jon Doe', 'jon.doe@magento.com', 'General', '310-111-1111', '+310-111-1111', 90232, '090232', - 'United States', 'California', '=', '-', '+' + 'United States', 'California', '=', '-', '+', ' 3111', '\t3111' ], ]; + /** + * @var string[] + */ protected $_testHeader = [ 'HeaderID', 'HeaderName', 'HeaderEmail', 'HeaderGroup', 'HeaderPhone', 'Header+Phone', 'HeaderZIP', - 'Header0ZIP', 'HeaderCountry', 'HeaderRegion', 'HeaderSymbol=', 'HeaderSymbol-', 'HeaderSymbol+' + 'Header0ZIP', 'HeaderCountry', 'HeaderRegion', 'HeaderSymbol=', 'HeaderSymbol-', 'HeaderSymbol+', + 'HeaderNumberWithSpace', 'HeaderNumberWithTabulation' ]; + /** + * @var string[] + */ protected $_testFooter = [ 'FooterID', 'FooterName', 'FooterEmail', 'FooterGroup', 'FooterPhone', 'Footer+Phone', 'FooterZIP', - 'Footer0ZIP', 'FooterCountry', 'FooterRegion', 'FooterSymbol=', 'FooterSymbol-', 'FooterSymbol+' + 'Footer0ZIP', 'FooterCountry', 'FooterRegion', 'FooterSymbol=', 'FooterSymbol-', 'FooterSymbol+', + 'FooterNumberWithSpace', 'FooterNumberWithTabulation' ]; /** diff --git a/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml b/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml index 7b551268d8995..41cd2eb7c3cf1 100644 --- a/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml +++ b/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml @@ -32,6 +32,8 @@ <Cell><Data ss:Type="String">HeaderSymbol=</Data></Cell> <Cell><Data ss:Type="String">HeaderSymbol-</Data></Cell> <Cell><Data ss:Type="String">HeaderSymbol+</Data></Cell> + <Cell><Data ss:Type="String">HeaderNumberWithSpace</Data></Cell> + <Cell><Data ss:Type="String">HeaderNumberWithTabulation</Data></Cell> </Row> <Row> <Cell><Data ss:Type="String">ID</Data></Cell> @@ -47,6 +49,8 @@ <Cell><Data ss:Type="String">Symbol=</Data></Cell> <Cell><Data ss:Type="String">Symbol-</Data></Cell> <Cell><Data ss:Type="String">Symbol+</Data></Cell> + <Cell><Data ss:Type="String">NumberWithSpace</Data></Cell> + <Cell><Data ss:Type="String">NumberWithTabulation</Data></Cell> </Row> <Row> <Cell><Data ss:Type="Number">1</Data></Cell> @@ -62,6 +66,8 @@ <Cell><Data ss:Type="String"> =</Data></Cell> <Cell><Data ss:Type="String"> -</Data></Cell> <Cell><Data ss:Type="String"> +</Data></Cell> + <Cell><Data ss:Type="String"> 3111</Data></Cell> + <Cell><Data ss:Type="String">\t3111</Data></Cell> </Row> <Row> <Cell><Data ss:Type="String">FooterID</Data></Cell> @@ -77,6 +83,8 @@ <Cell><Data ss:Type="String">FooterSymbol=</Data></Cell> <Cell><Data ss:Type="String">FooterSymbol-</Data></Cell> <Cell><Data ss:Type="String">FooterSymbol+</Data></Cell> + <Cell><Data ss:Type="String">FooterNumberWithSpace</Data></Cell> + <Cell><Data ss:Type="String">FooterNumberWithTabulation</Data></Cell> </Row> </Table> </Worksheet> diff --git a/lib/internal/Magento/Framework/Crontab/README.md b/lib/internal/Magento/Framework/Crontab/README.md index bfbf194715dc8..76e50b89da617 100644 --- a/lib/internal/Magento/Framework/Crontab/README.md +++ b/lib/internal/Magento/Framework/Crontab/README.md @@ -1,12 +1,14 @@ Library for working with crontab The library has the next interfaces: + * CrontabManagerInterface * TasksProviderInterface *CrontabManagerInterface* provides working with crontab: + * *getTasks* - get list of Magento cron tasks from crontab * *saveTasks* - save Magento cron tasks to crontab * *removeTasks* - remove Magento cron tasks from crontab -*TasksProviderInterface* has only one method *getTasks*. This interface provides transportation the list of tasks from DI \ No newline at end of file +*TasksProviderInterface* has only one method *getTasks*. This interface provides transportation the list of tasks from DI diff --git a/lib/internal/Magento/Framework/Css/README.md b/lib/internal/Magento/Framework/Css/README.md index 2a7b46812470c..d7ccad3a7d70c 100644 --- a/lib/internal/Magento/Framework/Css/README.md +++ b/lib/internal/Magento/Framework/Css/README.md @@ -1,3 +1,4 @@ # Overview + CSS library contains common infrastructure to work with style sheets. It provides an ability to process LESS files in Magento application and convert this dynamic stylesheet language into CSS using correspondent parser. diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 1f385b18f8671..46538efcff133 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -24,6 +24,7 @@ use Magento\Framework\DB\Sql\Expression; use Magento\Framework\DB\Statement\Parameter; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Phrase; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Setup\SchemaListener; @@ -44,7 +45,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface +class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface, ResetAfterRequestInterface { // @codingStandardsIgnoreEnd @@ -194,7 +195,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface protected $_queryHook = null; /** - * @var String + * @var StringUtils */ protected $string; @@ -280,6 +281,23 @@ public function __construct( } } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->_transactionLevel = 0; + $this->_isRolledBack = false; + $this->_connectionFlagsSet = false; + $this->_ddlCache = []; + $this->_bindParams = []; + $this->_bindIncrement = 0; + $this->_isDdlCacheAllowed = true; + $this->isMysql8Engine = null; + $this->_queryHook = null; + $this->closeConnection(); + } + /** * Begin new DB transaction for connection * @@ -1518,7 +1536,7 @@ public function select() /** * Quotes a value and places into a piece of text at a placeholder. * - * Method revrited for handle empty arrays in value param + * Method rewrited for handle empty arrays in value param * * @param string $text The text with a placeholder. * @param array|null|int|string|float|Expression|Select|\DateTimeInterface $value The value to quote. @@ -4144,4 +4162,15 @@ public function closeConnection() } parent::closeConnection(); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php b/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php index 2fd4883af5eb5..46025f400b1d2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php +++ b/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php @@ -31,6 +31,10 @@ class SqlVersionProvider public const MYSQL_8_0_29_VERSION = '8.0.29'; + public const MARIA_DB_10_6_11_VERSION = '10.6.11'; + + public const MARIA_DB_10_4_27_VERSION = '10.4.27'; + /**#@-*/ /** @@ -139,4 +143,38 @@ public function isMysqlGte8029(): bool } return false; } + + /** + * Check if MariaDB version is greater than equal to 10.6.11 + * + * @return bool + * @throws ConnectionException + */ + public function isMariaDBGte10611(): bool + { + $sqlVersion = $this->getSqlVersion(); + $isMariaDB106 = str_contains($sqlVersion, SqlVersionProvider::MARIA_DB_10_6_VERSION); + $sqlExactVersion = $this->fetchSqlVersion(ResourceConnection::DEFAULT_CONNECTION); + if ($isMariaDB106 && version_compare($sqlExactVersion, '10.6.11', '>=')) { + return true; + } + return false; + } + + /** + * Check if MariaDB version is greater than equal to 10.4.27 + * + * @return bool + * @throws ConnectionException + */ + public function isMariaDBGte10427(): bool + { + $sqlVersion = $this->getSqlVersion(); + $isMariaDB104 = str_contains($sqlVersion, SqlVersionProvider::MARIA_DB_10_4_VERSION); + $sqlExactVersion = $this->fetchSqlVersion(ResourceConnection::DEFAULT_CONNECTION); + if ($isMariaDB104 && version_compare($sqlExactVersion, '10.4.27', '>=')) { + return true; + } + return false; + } } diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 9a417b4f837ac..9432a9db9db4f 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection\EntityFactoryInterface; use Magento\Framework\DataObject; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Option\ArrayInterface; /** @@ -18,8 +19,14 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ -class Collection implements \IteratorAggregate, \Countable, ArrayInterface, CollectionDataSourceInterface +class Collection implements + \IteratorAggregate, + \Countable, + ArrayInterface, + CollectionDataSourceInterface, + ResetAfterRequestInterface { public const SORT_ORDER_ASC = 'ASC'; @@ -919,4 +926,19 @@ public function __wakeup() EntityFactoryInterface::class ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->clear(); + // TODO: Is it safe to move the following into clear() ? + $this->_orders = []; + $this->_filters = []; + $this->_isFiltersRendered = false; + $this->_curPage = 1; + $this->_pageSize = false; + $this->_flags = []; + } } diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index b829f063ac2de..4ce4156e72fd2 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Data\Collection; use Magento\Framework\App\ResourceConnection; @@ -19,6 +21,7 @@ * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ abstract class AbstractDb extends \Magento\Framework\Data\Collection @@ -99,6 +102,170 @@ abstract class AbstractDb extends \Magento\Framework\Data\Collection */ protected $extensionAttributesJoinProcessor; + /** + * @var array + * @see https://en.wikipedia.org/wiki/List_of_SQL_reserved_words + */ + private array $sqlReservedWords = [ + 'ABORT', 'ABORTSESSION', 'ABS', 'ABSENT', 'ABSOLUTE', 'ACCESS', + 'ACCESS_LOCK', 'ACCESSIBLE', 'ACCOUNT', 'ACOS', 'ACOSH', 'ACTION', + 'ADD', 'ADD_MONTHS', 'ADMIN', 'AFTER', 'AGGREGATE', 'ALIAS', 'ALL', + 'ALLOCATE', 'ALLOW', 'ALTER', 'ALTERAND', 'AMP', 'ANALYSE', 'ANALYZE', + 'AND', 'ANSIDATE', 'ANY', 'ARE', 'ARRAY', 'ARRAY_AGG', 'ARRAY_EXISTS', + 'ARRAY_MAX_CARDINALITY', 'AS', 'ASC', 'ASENSITIVE', 'ASIN', 'ASINH', + 'ASSERTION', 'ASSOCIATE', 'ASUTIME', 'ASYMMETRIC', 'AT', 'ATAN', + 'ATAN2', 'ATANH', 'ATOMIC', 'AUDIT', 'AUTHORIZATION', 'AUX', + 'AUXILIARY', 'AVE', 'AVERAGE', 'AVG', 'BACKUP', 'BEFORE', 'BEGIN', + 'BEGIN_FRAME', 'BEGIN_PARTITION', 'BETWEEN', 'BIGINT', 'BINARY', 'BIT', + 'BLOB', 'BOOLEAN', 'BOTH', 'BREADTH', 'BREAK', 'BROWSE', 'BT', + 'BUFFERPOOL', 'BULK', 'BUT', 'BY', 'BYTE', 'BYTEINT', 'BYTES', 'CALL', + 'CALLED', 'CAPTURE', 'CARDINALITY', 'CASCADE', 'CASCADED', 'CASE', + 'CASE_N', 'CASESPECIFIC', 'CAST', 'CATALOG', 'CCSID', 'CD', 'CEIL', + 'CEILING', 'CHANGE', 'CHAR', 'CHAR_LENGTH', 'CHAR2HEXINT', 'CHARACTER', + 'CHARACTER_LENGTH', 'CHARACTERS', 'CHARS', 'CHECK', 'CHECKPOINT', + 'CLASS', 'CLASSIFIER', 'CLOB', 'CLONE', 'CLOSE', 'CLUSTER', + 'CLUSTERED', 'CM', 'COALESCE', 'COLLATE', 'COLLATION', 'COLLECT', + 'COLLECTION', 'COLLID', 'COLUMN', 'COLUMN_VALUE', 'COMMENT', 'COMMIT', + 'COMPLETION', 'COMPRESS', 'COMPUTE', 'CONCAT', 'CONCURRENTLY', + 'CONDITION', 'CONNECT', 'CONNECTION', 'CONSTRAINT', 'CONSTRAINTS', + 'CONSTRUCTOR', 'CONTAINS', 'CONTAINSTABLE', 'CONTENT', 'CONTINUE', + 'CONVERT', 'CONVERT_TABLE_HEADER', 'COPY', 'CORR', 'CORRESPONDING', + 'COS', 'COSH', 'COUNT', 'COVAR_POP', 'COVAR_SAMP', 'CREATE', 'CROSS', + 'CS', 'CSUM', 'CT', 'CUBE', 'CUME_DIST', 'CURRENT', 'CURRENT_CATALOG', + 'CURRENT_DATE', 'CURRENT_DEFAULT_TRANSFORM_GROUP', 'CURRENT_LC_CTYPE', + 'CURRENT_PATH', 'CURRENT_ROLE', 'CURRENT_ROW', 'CURRENT_SCHEMA', + 'CURRENT_SERVER', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', + 'CURRENT_TIMEZONE', 'CURRENT_TRANSFORM_GROUP_FOR_TYPE', 'CURRENT_USER', + 'CURRVAL', 'CURSOR', 'CV', 'CYCLE', 'DATA', 'DATABASE', 'DATABASES', + 'DATABLOCKSIZE', 'DATE', 'DATEFORM', 'DAY', 'DAY_HOUR', + 'DAY_MICROSECOND', 'DAY_MINUTE', 'DAY_SECOND', 'DAYS', 'DBCC', + 'DBINFO', 'DEALLOCATE', 'DEC', 'DECFLOAT', 'DECIMAL', 'DECLARE', + 'DEFAULT', 'DEFERRABLE', 'DEFERRED', 'DEFINE', 'DEGREES', 'DEL', + 'DELAYED', 'DELETE', 'DENSE_RANK', 'DENY', 'DEPTH', 'DEREF', 'DESC', + 'DESCRIBE', 'DESCRIPTOR', 'DESTROY', 'DESTRUCTOR', 'DETERMINISTIC', + 'DIAGNOSTIC', 'DIAGNOSTICS', 'DICTIONARY', 'DISABLE', 'DISABLED', + 'DISALLOW', 'DISCONNECT', 'DISK', 'DISTINCT', 'DISTINCTROW', + 'DISTRIBUTED', 'DIV', 'DO', 'DOCUMENT', 'DOMAIN', 'DOUBLE', 'DROP', + 'DSSIZE', 'DUAL', 'DUMP', 'DYNAMIC', 'EACH', 'ECHO', 'EDITPROC', + 'ELEMENT', 'ELSE', 'ELSEIF', 'EMPTY', 'ENABLED', 'ENCLOSED', + 'ENCODING', 'ENCRYPTION', 'END', 'END_FRAME', 'END_PARTITION', + 'END-EXEC', 'ENDING', 'EQ', 'EQUALS', 'ERASE', 'ERRLVL', 'ERROR', + 'ERRORFILES', 'ERRORTABLES', 'ESCAPE', 'ESCAPED', 'ET', 'EVERY', + 'EXCEPT', 'EXCEPTION', 'EXCLUSIVE', 'EXEC', 'EXECUTE', 'EXISTS', + 'EXIT', 'EXP', 'EXPLAIN', 'EXTERNAL', 'EXTRACT', 'FALLBACK', 'FALSE', + 'FASTEXPORT', 'FENCED', 'FETCH', 'FIELDPROC', 'FILE', 'FILLFACTOR', + 'FILTER', 'FINAL', 'FIRST', 'FIRST_VALUE', 'FLOAT', 'FLOAT4', 'FLOAT8', + 'FLOOR', 'FOR', 'FORCE', 'FOREIGN', 'FORMAT', 'FOUND', 'FRAME_ROW', + 'FREE', 'FREESPACE', 'FREETEXT', 'FREETEXTTABLE', 'FREEZE', 'FROM', + 'FULL', 'FULLTEXT', 'FUNCTION', 'FUSION', 'GE', 'GENERAL', 'GENERATED', + 'GET', 'GIVE', 'GLOBAL', 'GO', 'GOTO', 'GRANT', 'GRAPHIC', 'GROUP', + 'GROUPING', 'GROUPS', 'GT', 'HANDLER', 'HASH', 'HASHAMP', 'HASHBAKAMP', + 'HASHBUCKET', 'HASHROW', 'HAVING', 'HELP', 'HIGH_PRIORITY', 'HOLD', + 'HOLDLOCK', 'HOST', 'HOUR', 'HOUR_MICROSECOND', 'HOUR_MINUTE', + 'HOUR_SECOND', 'HOURS', 'IDENTIFIED', 'IDENTITY', 'IDENTITY_INSERT', + 'IDENTITYCOL', 'IF', 'IGNORE', 'ILIKE', 'IMMEDIATE', 'IN', 'INCLUSIVE', + 'INCONSISTENT', 'INCREMENT', 'INDEX', 'INDICATOR', 'INFILE', 'INHERIT', + 'INITIAL', 'INITIALIZE', 'INITIALLY', 'INITIATE', 'INNER', 'INOUT', + 'INPUT', 'INS', 'INSENSITIVE', 'INSERT', 'INSTEAD', 'INT', 'INT1', + 'INT2', 'INT3', 'INT4', 'INT8', 'INTEGER', 'INTEGERDATE', 'INTERSECT', + 'INTERSECTION', 'INTERVAL', 'INTO', 'IO_AFTER_GTIDS', + 'IO_BEFORE_GTIDS', 'IS', 'ISNULL', 'ISOBID', 'ISOLATION', 'ITERATE', + 'JAR', 'JOIN', 'JOURNAL', 'JSON', 'JSON_ARRAY', 'JSON_ARRAYAGG', + 'JSON_EXISTS', 'JSON_OBJECT', 'JSON_OBJECTAGG', 'JSON_QUERY', + 'JSON_TABLE', 'JSON_TABLE_PRIMITIVE', 'JSON_VALUE', 'KEEP', 'KEY', + 'KEYS', 'KILL', 'KURTOSIS', 'LABEL', 'LAG', 'LANGUAGE', 'LARGE', + 'LAST', 'LAST_VALUE', 'LATERAL', 'LC_CTYPE', 'LE', 'LEAD', 'LEADING', + 'LEAVE', 'LEFT', 'LESS', 'LEVEL', 'LIKE', 'LIKE_REGEX', 'LIMIT', + 'LINEAR', 'LINENO', 'LINES', 'LISTAGG', 'LN', 'LOAD', 'LOADING', + 'LOCAL', 'LOCALE', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCATOR', + 'LOCATORS', 'LOCK', 'LOCKING', 'LOCKMAX', 'LOCKSIZE', 'LOG', 'LOG10', + 'LOGGING', 'LOGON', 'LONG', 'LONGBLOB', 'LONGTEXT', 'LOOP', + 'LOW_PRIORITY', 'LOWER', 'LT', 'MACRO', 'MAINTAINED', 'MAP', + 'MASTER_BIND', 'MASTER_SSL_VERIFY_SERVER_CERT', 'MATCH', + 'MATCH_NUMBER', 'MATCH_RECOGNIZE', 'MATCHES', 'MATERIALIZED', 'MAVG', + 'MAX', 'MAXEXTENTS', 'MAXIMUM', 'MAXVALUE', 'MCHARACTERS', 'MDIFF', + 'MEDIUMBLOB', 'MEDIUMINT', 'MEDIUMTEXT', 'MEMBER', 'MERGE', 'METHOD', + 'MICROSECOND', 'MICROSECONDS', 'MIDDLEINT', 'MIN', 'MINDEX', 'MINIMUM', + 'MINUS', 'MINUTE', 'MINUTE_MICROSECOND', 'MINUTE_SECOND', 'MINUTES', + 'MLINREG', 'MLOAD', 'MLSLABEL', 'MOD', 'MODE', 'MODIFIES', 'MODIFY', + 'MODULE', 'MONITOR', 'MONRESOURCE', 'MONSESSION', 'MONTH', 'MONTHS', + 'MSUBSTR', 'MSUM', 'MULTISET', 'NAMED', 'NAMES', 'NATIONAL', 'NATURAL', + 'NCHAR', 'NCLOB', 'NE', 'NESTED_TABLE_ID', 'NEW', 'NEW_TABLE', 'NEXT', + 'NEXTVAL', 'NO', 'NO_WRITE_TO_BINLOG', 'NOAUDIT', 'NOCHECK', + 'NOCOMPRESS', 'NONCLUSTERED', 'NONE', 'NORMALIZE', 'NOT', 'NOTNULL', + 'NOWAIT', 'NTH_VALUE', 'NTILE', 'NULL', 'NULLIF', 'NULLIFZERO', + 'NULLS', 'NUMBER', 'NUMERIC', 'NUMPARTS', 'OBID', 'OBJECT', 'OBJECTS', + 'OCCURRENCES_REGEX', 'OCTET_LENGTH', 'OF', 'OFF', 'OFFLINE', 'OFFSET', + 'OFFSETS', 'OLD', 'OLD_TABLE', 'OMIT', 'ON', 'ONE', 'ONLINE', 'ONLY', + 'OPEN', 'OPENDATASOURCE', 'OPENQUERY', 'OPENROWSET', 'OPENXML', + 'OPERATION', 'OPTIMIZATION', 'OPTIMIZE', 'OPTIMIZER_COSTS', 'OPTION', + 'OPTIONALLY', 'OR', 'ORDER', 'ORDINALITY', 'ORGANIZATION', 'OUT', + 'OUTER', 'OUTFILE', 'OUTPUT', 'OVER', 'OVERLAPS', 'OVERLAY', + 'OVERRIDE', 'PACKAGE', 'PAD', 'PADDED', 'PARAMETER', 'PARAMETERS', + 'PART', 'PARTIAL', 'PARTITION', 'PARTITIONED', 'PARTITIONING', + 'PASSWORD', 'PATH', 'PATTERN', 'PCTFREE', 'PER', 'PERCENT', + 'PERCENT_RANK', 'PERCENTILE_CONT', 'PERCENTILE_DISC', 'PERIOD', 'PERM', + 'PERMANENT', 'PIECESIZE', 'PIVOT', 'PLACING', 'PLAN', 'PORTION', + 'POSITION', 'POSITION_REGEX', 'POSTFIX', 'POWER', 'PRECEDES', + 'PRECISION', 'PREFIX', 'PREORDER', 'PREPARE', 'PRESERVE', 'PREVVAL', + 'PRIMARY', 'PRINT', 'PRIOR', 'PRIQTY', 'PRIVATE', 'PRIVILEGES', 'PROC', + 'PROCEDURE', 'PROFILE', 'PROGRAM', 'PROPORTIONAL', 'PROTECTION', + 'PSID', 'PTF', 'PUBLIC', 'PURGE', 'QUALIFIED', 'QUALIFY', 'QUANTILE', + 'QUERY', 'QUERYNO', 'RADIANS', 'RAISERROR', 'RANDOM', 'RANGE', + 'RANGE_N', 'RANK', 'RAW', 'READ', 'READ_WRITE', 'READS', 'READTEXT', + 'REAL', 'RECONFIGURE', 'RECURSIVE', 'REF', 'REFERENCES', 'REFERENCING', + 'REFRESH', 'REGEXP', 'REGR_AVGX', 'REGR_AVGY', 'REGR_COUNT', + 'REGR_INTERCEPT', 'REGR_R2', 'REGR_SLOPE', 'REGR_SXX', 'REGR_SXY', + 'REGR_SYY', 'RELATIVE', 'RELEASE', 'RENAME', 'REPEAT', 'REPLACE', + 'REPLICATION', 'REPOVERRIDE', 'REQUEST', 'REQUIRE', 'RESIGNAL', + 'RESOURCE', 'RESTART', 'RESTORE', 'RESTRICT', 'RESULT', + 'RESULT_SET_LOCATOR', 'RESUME', 'RET', 'RETRIEVE', 'RETURN', + 'RETURNING', 'RETURNS', 'REVALIDATE', 'REVERT', 'REVOKE', 'RIGHT', + 'RIGHTS', 'RLIKE', 'ROLE', 'ROLLBACK', 'ROLLFORWARD', 'ROLLUP', + 'ROUND_CEILING', 'ROUND_DOWN', 'ROUND_FLOOR', 'ROUND_HALF_DOWN', + 'ROUND_HALF_EVEN', 'ROUND_HALF_UP', 'ROUND_UP', 'ROUTINE', 'ROW', + 'ROW_NUMBER', 'ROWCOUNT', 'ROWGUIDCOL', 'ROWID', 'ROWNUM', 'ROWS', + 'ROWSET', 'RULE', 'RUN', 'RUNNING', 'SAMPLE', 'SAMPLEID', 'SAVE', + 'SAVEPOINT', 'SCHEMA', 'SCHEMAS', 'SCOPE', 'SCRATCHPAD', 'SCROLL', + 'SEARCH', 'SECOND', 'SECOND_MICROSECOND', 'SECONDS', 'SECQTY', + 'SECTION', 'SECURITY', 'SECURITYAUDIT', 'SEEK', 'SEL', 'SELECT', + 'SEMANTICKEYPHRASETABLE', 'SEMANTICSIMILARITYDETAILSTABLE', + 'SEMANTICSIMILARITYTABLE', 'SENSITIVE', 'SEPARATOR', 'SEQUENCE', + 'SESSION', 'SESSION_USER', 'SET', 'SETRESRATE', 'SETS', 'SETSESSRATE', + 'SETUSER', 'SHARE', 'SHOW', 'SHUTDOWN', 'SIGNAL', 'SIMILAR', 'SIMPLE', + 'SIN', 'SINH', 'SIZE', 'SKEW', 'SKIP', 'SMALLINT', 'SOME', 'SOUNDEX', + 'SOURCE', 'SPACE', 'SPATIAL', 'SPECIFIC', 'SPECIFICTYPE', 'SPOOL', + 'SQL', 'SQL_BIG_RESULT', 'SQL_CALC_FOUND_ROWS', 'SQL_SMALL_RESULT', + 'SQLEXCEPTION', 'SQLSTATE', 'SQLTEXT', 'SQLWARNING', 'SQRT', 'SS', + 'SSL', 'STANDARD', 'START', 'STARTING', 'STARTUP', 'STATE', + 'STATEMENT', 'STATIC', 'STATISTICS', 'STAY', 'STDDEV_POP', + 'STDDEV_SAMP', 'STEPINFO', 'STOGROUP', 'STORED', 'STORES', + 'STRAIGHT_JOIN', 'STRING_CS', 'STRUCTURE', 'STYLE', 'SUBMULTISET', + 'SUBSCRIBER', 'SUBSET', 'SUBSTR', 'SUBSTRING', 'SUBSTRING_REGEX', + 'SUCCEEDS', 'SUCCESSFUL', 'SUM', 'SUMMARY', 'SUSPEND', 'SYMMETRIC', + 'SYNONYM', 'SYSDATE', 'SYSTEM', 'SYSTEM_TIME', 'SYSTEM_USER', + 'SYSTIMESTAMP', 'TABLE', 'TABLESAMPLE', 'TABLESPACE', 'TAN', 'TANH', + 'TBL_CS', 'TEMPORARY', 'TERMINATE', 'TERMINATED', 'TEXTSIZE', 'THAN', + 'THEN', 'THRESHOLD', 'TIME', 'TIMESTAMP', 'TIMEZONE_HOUR', + 'TIMEZONE_MINUTE', 'TINYBLOB', 'TINYINT', 'TINYTEXT', 'TITLE', 'TO', + 'TOP', 'TRACE', 'TRAILING', 'TRAN', 'TRANSACTION', 'TRANSLATE', + 'TRANSLATE_CHK', 'TRANSLATE_REGEX', 'TRANSLATION', 'TREAT', 'TRIGGER', + 'TRIM', 'TRIM_ARRAY', 'TRUE', 'TRUNCATE', 'TRY_CONVERT', 'TSEQUAL', + 'TYPE', 'UC', 'UESCAPE', 'UID', 'UNDEFINED', 'UNDER', 'UNDO', 'UNION', + 'UNIQUE', 'UNKNOWN', 'UNLOCK', 'UNNEST', 'UNPIVOT', 'UNSIGNED', + 'UNTIL', 'UPD', 'UPDATE', 'UPDATETEXT', 'UPPER', 'UPPERCASE', 'USAGE', + 'USE', 'USER', 'USING', 'UTC_DATE', 'UTC_TIME', 'UTC_TIMESTAMP', + 'VALIDATE', 'VALIDPROC', 'VALUE', 'VALUE_OF', 'VALUES', 'VAR_POP', + 'VAR_SAMP', 'VARBINARY', 'VARBYTE', 'VARCHAR', 'VARCHAR2', + 'VARCHARACTER', 'VARGRAPHIC', 'VARIABLE', 'VARIADIC', 'VARIANT', + 'VARYING', 'VCAT', 'VERBOSE', 'VERSIONING', 'VIEW', 'VIRTUAL', + 'VOLATILE', 'VOLUMES', 'WAIT', 'WAITFOR', 'WHEN', 'WHENEVER', 'WHERE', + 'WHILE', 'WIDTH_BUCKET', 'WINDOW', 'WITH', 'WITHIN', 'WITHIN_GROUP', + 'WITHOUT', 'WLM', 'WORK', 'WRITE', 'WRITETEXT', 'XMLCAST', 'XMLEXISTS', + 'XMLNAMESPACES', 'XOR', 'YEAR', 'YEAR_MONTH', 'YEARS', 'ZEROFILL', + 'ZEROIFNULL', 'ZONE', + ]; + /** * @param EntityFactoryInterface $entityFactory * @param Logger $logger @@ -116,7 +283,25 @@ public function __construct( if ($connection !== null) { $this->setConnection($connection); } + $this->_logger = $logger; + $this->sqlReservedWords = array_flip($this->sqlReservedWords); + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->setConnection($this->_conn); + // Note: not resetting _idFieldName because some subclasses define it class property + $this->_bindParams = []; + $this->_data = null; + // Note: not resetting _map because some subclasses define it class property but not _construct method. + $this->_fetchStmt = null; + $this->_isOrdersRendered = false; + $this->extensionAttributesJoinProcessor = null; } /** @@ -131,6 +316,7 @@ abstract public function getResource(); * * @param string $name * @param mixed $value + * * @return $this */ public function addBindParam($name, $value) @@ -143,6 +329,7 @@ public function addBindParam($name, $value) * Specify collection objects id field name * * @param string $fieldName + * * @return $this */ protected function _setIdFieldName($fieldName) @@ -165,6 +352,7 @@ public function getIdFieldName() * Get collection item identifier * * @param \Magento\Framework\DataObject $item + * * @return mixed */ protected function _getItemId(\Magento\Framework\DataObject $item) @@ -172,6 +360,7 @@ protected function _getItemId(\Magento\Framework\DataObject $item) if ($field = $this->getIdFieldName()) { return $item->getData($field); } + return parent::_getItemId($item); } @@ -179,6 +368,7 @@ protected function _getItemId(\Magento\Framework\DataObject $item) * Set database connection adapter * * @param \Magento\Framework\DB\Adapter\AdapterInterface $conn + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -221,6 +411,7 @@ public function getSize() $sql = $this->getSelectCountSql(); $this->_totalRecords = $this->_totalRecords ?? $this->getConnection()->fetchOne($sql, $this->_bindParams); } + return (int)$this->_totalRecords; } @@ -247,30 +438,33 @@ public function getSelectCountSql() $countSelect->reset(\Magento\Framework\DB\Select::GROUP); $group = $this->getSelect()->getPart(\Magento\Framework\DB\Select::GROUP); - $countSelect->columns(new \Zend_Db_Expr(("COUNT(DISTINCT ".implode(", ", $group).")"))); + $countSelect->columns(new \Zend_Db_Expr(("COUNT(DISTINCT " . implode(", ", $group) . ")"))); return $countSelect; } /** * Get sql select string or object * - * @param bool $stringMode - * @return string|\Magento\Framework\DB\Select + * @param bool $stringMode + * + * @return string|\Magento\Framework\DB\Select */ public function getSelectSql($stringMode = false) { if ($stringMode) { return $this->_select->__toString(); } + return $this->_select; } /** * Add select order * - * @param string $field - * @param string $direction - * @return $this + * @param string $field + * @param string $direction + * + * @return $this */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) { @@ -282,6 +476,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) * * @param string $field * @param string $direction + * * @return $this */ public function addOrder($field, $direction = self::SORT_ORDER_DESC) @@ -294,6 +489,7 @@ public function addOrder($field, $direction = self::SORT_ORDER_DESC) * * @param string $field * @param string $direction + * * @return $this */ public function unshiftOrder($field, $direction = self::SORT_ORDER_DESC) @@ -307,6 +503,7 @@ public function unshiftOrder($field, $direction = self::SORT_ORDER_DESC) * @param string $field * @param string $direction * @param bool $unshift + * * @return $this */ private function _setOrder($field, $direction, $unshift = false) @@ -322,10 +519,12 @@ private function _setOrder($field, $direction, $unshift = false) foreach ($this->_orders as $key => $dir) { $orders[$key] = $dir; } + $this->_orders = $orders; } else { $this->_orders[$field] = $direction; } + return $this; } @@ -361,6 +560,7 @@ protected function _renderFilters() $this->_select->where($condition); } } + $this->_isFiltersRendered = true; return $this; } @@ -383,6 +583,7 @@ protected function _renderFiltersBefore() * * @param string|array $field * @param null|string|array $condition + * * @return $this */ public function addFieldToFilter($field, $condition = null) @@ -406,9 +607,10 @@ public function addFieldToFilter($field, $condition = null) /** * Build sql where condition part * - * @param string|array $field - * @param null|string|array $condition - * @return string + * @param string|array $field + * @param null|string|array $condition + * + * @return string */ protected function _translateCondition($field, $condition) { @@ -419,8 +621,9 @@ protected function _translateCondition($field, $condition) /** * Try to get mapped field name for filter to collection * - * @param string $field - * @return string + * @param string $field + * + * @return string */ protected function _getMappedField($field) { @@ -478,6 +681,7 @@ protected function _getMapper() * * @param string $fieldName * @param integer|string|array $condition + * * @return string */ protected function _getConditionSql($fieldName, $condition) @@ -489,6 +693,7 @@ protected function _getConditionSql($fieldName, $condition) * Return the field name for the condition. * * @param string $fieldName + * * @return string */ protected function _getConditionFieldName($fieldName) @@ -505,8 +710,13 @@ protected function _renderOrders() { if (!$this->_isOrdersRendered) { foreach ($this->_orders as $field => $direction) { + if (isset($this->sqlReservedWords[strtoupper($field)])) { + $field = "`$field`"; + } + $this->_select->order(new \Zend_Db_Expr($field . ' ' . $direction)); } + $this->_isOrdersRendered = true; } @@ -530,8 +740,9 @@ protected function _renderLimit() /** * Set select distinct * - * @param bool $flag - * @return $this + * @param bool $flag + * + * @return $this */ public function distinct($flag) { @@ -552,9 +763,10 @@ protected function _beforeLoad() /** * Load data * - * @param bool $printQuery - * @param bool $logQuery - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * + * @return $this */ public function load($printQuery = false, $logQuery = false) { @@ -568,9 +780,10 @@ public function load($printQuery = false, $logQuery = false) /** * Load data with filter in place * - * @param bool $printQuery - * @param bool $logQuery - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * + * @return $this */ public function loadWithFilter($printQuery = false, $logQuery = false) { @@ -585,11 +798,13 @@ public function loadWithFilter($printQuery = false, $logQuery = false) if ($this->getIdFieldName()) { $item->setIdFieldName($this->getIdFieldName()); } + $item->addData($row); $this->beforeAddLoadedItem($item); $this->addItem($item); } } + $this->_setIsLoaded(); $this->_afterLoad(); return $this; @@ -599,6 +814,7 @@ public function loadWithFilter($printQuery = false, $logQuery = false) * Let do something before add loaded item in collection * * @param \Magento\Framework\DataObject $item + * * @return \Magento\Framework\DataObject */ protected function beforeAddLoadedItem(\Magento\Framework\DataObject $item) @@ -620,16 +836,19 @@ public function fetchItem() $this->_fetchStmt = $this->getConnection()->query($this->getSelect()); } + $data = $this->_fetchStmt->fetch(); if (!empty($data) && is_array($data)) { $item = $this->getNewEmptyItem(); if ($this->getIdFieldName()) { $item->setIdFieldName($this->getIdFieldName()); } + $item->setData($data); return $item; } + return false; } @@ -639,6 +858,7 @@ public function fetchItem() * @param string|null $valueField * @param string $labelField * @param array $additional + * * @return array */ protected function _toOptionArray($valueField = null, $labelField = 'name', $additional = []) @@ -646,21 +866,24 @@ protected function _toOptionArray($valueField = null, $labelField = 'name', $add if ($valueField === null) { $valueField = $this->getIdFieldName(); } + return parent::_toOptionArray($valueField, $labelField, $additional); } /** * Overridden to use _idFieldName by default. * - * @param string $valueField - * @param string $labelField - * @return array + * @param string $valueField + * @param string $labelField + * + * @return array */ protected function _toOptionHash($valueField = null, $labelField = 'name') { if ($valueField === null) { $valueField = $this->getIdFieldName(); } + return parent::_toOptionHash($valueField, $labelField); } @@ -677,6 +900,7 @@ public function getData() $this->_data = $this->_fetchAll($select); $this->_afterLoadData(); } + return $this->_data; } @@ -716,6 +940,7 @@ protected function _afterLoad() * * @param bool $printQuery * @param bool $logQuery + * * @return $this */ public function loadData($printQuery = false, $logQuery = false) @@ -726,10 +951,11 @@ public function loadData($printQuery = false, $logQuery = false) /** * Print and/or log query * - * @param bool $printQuery - * @param bool $logQuery - * @param string $sql - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * @param string $sql + * + * @return $this */ public function printLogQuery($printQuery = false, $logQuery = false, $sql = null) { @@ -741,6 +967,7 @@ public function printLogQuery($printQuery = false, $logQuery = false, $sql = nul if ($logQuery || $this->getFlag('log_query')) { $this->_logQuery($sql); } + return $this; } @@ -748,6 +975,7 @@ public function printLogQuery($printQuery = false, $logQuery = false, $sql = nul * Log query * * @param string $sql + * * @return void */ protected function _logQuery($sql) @@ -775,6 +1003,7 @@ protected function _reset() * Fetch collection data * * @param Select $select + * * @return array */ protected function _fetchAll(Select $select) @@ -788,6 +1017,7 @@ protected function _fetchAll(Select $select) ); } } + return $data; } @@ -796,7 +1026,8 @@ protected function _fetchAll(Select $select) * * @param string $filter * @param string $alias - * @param string $group default 'fields' + * @param string $group Default: 'fields'. + * * @return $this */ public function addFilterToMap($filter, $alias, $group = 'fields') @@ -806,6 +1037,7 @@ public function addFilterToMap($filter, $alias, $group = 'fields') } elseif (empty($this->_map[$group])) { $this->_map[$group] = []; } + $this->_map[$group][$filter] = $alias; return $this; @@ -840,6 +1072,7 @@ protected function _initSelect() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock * * @param JoinDataInterface $join * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * * @return $this */ public function joinExtensionAttribute( @@ -857,12 +1090,14 @@ public function joinExtensionAttribute( [] ); } + $columns = []; foreach ($join->getSelectFields() as $selectField) { $fieldWIthDbPrefix = $selectField[JoinDataInterface::SELECT_FIELD_WITH_DB_PREFIX]; $columns[$selectField[JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS]] = $fieldWIthDbPrefix; $this->addFilterToMap($selectField[JoinDataInterface::SELECT_FIELD_EXTERNAL_ALIAS], $fieldWIthDbPrefix); } + $this->getSelect()->columns($columns); $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; return $this; @@ -891,6 +1126,7 @@ private function getMainTableAlias() return $tableAlias; } } + throw new \LogicException("Main table cannot be identified."); } diff --git a/lib/internal/Magento/Framework/Data/README.md b/lib/internal/Magento/Framework/Data/README.md index b8913069b396b..07dd49bd36dd1 100644 --- a/lib/internal/Magento/Framework/Data/README.md +++ b/lib/internal/Magento/Framework/Data/README.md @@ -11,18 +11,17 @@ **Data interpreter** is responsible for computation of effective value, i.e. evaluation of input data. Each individual interpreter recognizes only one particular type of input data. *Magento\Framework\Data\Argument\Interpreter\Composite* is used to dynamically choose which of underlying interpreters to delegate evaluation to. Child interpreters can be registered in it via constructor and later on through the adder method of its public interface. Each sub-interpreter is associated with a unique name during adding to the composite. In order to make a decision of which interpreter to use, input data has to carry an extra metadata – data key carrying name of an interpreter to use. Metadata value is intended for the composite interpreter only, thus it's not passed down to underlying interpreters. Data interpreters are used for handling DI arguments and layout arguments. ## Supported Data Structures - + ### Data Collections **Data Collection** is traversable, countable, ordered list. Class *Magento\Framework\Data\Collection* is at the top of the collections hierarchy. Every collection in the system is its descendant, directly or indirectly. - - * Database Data Collections are used to load items from a database. Two fetching strategies are supported in this library: - * Cache fetching strategy - retrieve data from cache - * Query fetching strategy - retrieve data from database - - * Filesystem Data Collection is used to scan a folder for files and/or folders. + +* Database Data Collections are used to load items from a database. Two fetching strategies are supported in this library: + * Cache fetching strategy - retrieve data from cache + * Query fetching strategy - retrieve data from database +* Filesystem Data Collection is used to scan a folder for files and/or folders. -### DataArray +### DataArray **DataArray** is data container with array access. @@ -36,4 +35,4 @@ ### Structure -**Structure** is a hierarchical data structure of elements. A structure contains elements; elements can be grouped into groups under the structure. It is used in layout. \ No newline at end of file +**Structure** is a hierarchical data structure of elements. A structure contains elements; elements can be grouped into groups under the structure. It is used in layout. diff --git a/lib/internal/Magento/Framework/Data/Structure.php b/lib/internal/Magento/Framework/Data/Structure.php index b22395f1ba835..8e9feac48b9e9 100644 --- a/lib/internal/Magento/Framework/Data/Structure.php +++ b/lib/internal/Magento/Framework/Data/Structure.php @@ -7,20 +7,21 @@ namespace Magento\Framework\Data; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * An associative data structure, that features "nested set" parent-child relations */ -class Structure +class Structure implements ResetAfterRequestInterface { /** * Reserved keys for storing structural relations */ - const PARENT = 'parent'; + public const PARENT = 'parent'; - const CHILDREN = 'children'; + public const CHILDREN = 'children'; - const GROUPS = 'groups'; + public const GROUPS = 'groups'; /** * @var array @@ -377,14 +378,14 @@ public function reorderChild($parentId, $childId, $position) $offset = $position; if ($position > 0) { if ($position >= $currentOffset + 1) { - $offset -= 1; + --$offset; } } elseif ($position < 0) { if ($position < $currentOffset + 1 - count($this->_elements[$parentId][self::CHILDREN])) { if ($position === -1) { $offset = null; } else { - $offset += 1; + ++$offset; } } } @@ -433,7 +434,7 @@ private function _getRelativeOffset($parentId, $siblingId, $delta) { $newOffset = $this->_getChildOffset($parentId, $siblingId) + $delta; if ($delta < 0) { - $newOffset += 1; + ++$newOffset; } if ($newOffset < 0) { $newOffset = 0; @@ -673,4 +674,12 @@ private function _assertArray($value) ); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_elements = []; + } } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Collection/DbTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Collection/DbTest.php index e496a853655db..219fd079aef0f 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Collection/DbTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Collection/DbTest.php @@ -92,12 +92,14 @@ public function testSetAddOrder() $this->collection->addOrder('some_field', Collection::SORT_ORDER_ASC); $this->collection->setOrder('other_field', Collection::SORT_ORDER_ASC); $this->collection->addOrder('other_field', Collection::SORT_ORDER_DESC); + $this->collection->addOrder('group', Collection::SORT_ORDER_ASC); $this->collection->load(); $selectOrders = $select->getPart(Select::ORDER); $this->assertEquals(['select_field', 'ASC'], array_shift($selectOrders)); $this->assertEquals('some_field ASC', (string)array_shift($selectOrders)); $this->assertEquals('other_field DESC', (string)array_shift($selectOrders)); + $this->assertEquals('`group` ASC', (string)array_shift($selectOrders)); // Reserved words need to be quoted $this->assertEmpty(array_shift($selectOrders)); } diff --git a/lib/internal/Magento/Framework/DataObject.php b/lib/internal/Magento/Framework/DataObject.php index d0ac9b73f08f8..554b16bd1dc15 100644 --- a/lib/internal/Magento/Framework/DataObject.php +++ b/lib/internal/Magento/Framework/DataObject.php @@ -12,6 +12,7 @@ * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 */ +#[\AllowDynamicProperties] //@phpstan-ignore-line class DataObject implements \ArrayAccess { /** @@ -551,4 +552,19 @@ public function offsetGet($offset) } return null; } + + /** + * Export only scalar and arrays properties for var_dump + * + * @return array + */ + public function __debugInfo() + { + return array_filter( + $this->_data, + function ($v) { + return is_scalar($v) || is_array($v); + } + ); + } } diff --git a/lib/internal/Magento/Framework/DataObject/README.md b/lib/internal/Magento/Framework/DataObject/README.md index 624c4e759be41..4a59f6bf0403f 100644 --- a/lib/internal/Magento/Framework/DataObject/README.md +++ b/lib/internal/Magento/Framework/DataObject/README.md @@ -1,4 +1,4 @@ # Object **Object** library contains functionality for mapping, management and the creation of dynamic member objects. -This also includes the objects which merge, read and make available xml configurations. \ No newline at end of file +This also includes the objects which merge, read and make available xml configurations. diff --git a/lib/internal/Magento/Framework/Encryption/README.md b/lib/internal/Magento/Framework/Encryption/README.md index a2bc6aa0f67f3..1f77c341417bd 100644 --- a/lib/internal/Magento/Framework/Encryption/README.md +++ b/lib/internal/Magento/Framework/Encryption/README.md @@ -1 +1 @@ -The Encryption library provides functionalities such as hashing passwords, encrypting/decrypting data, URLs encoding, using cryptographic algorithms. \ No newline at end of file +The Encryption library provides functionalities such as hashing passwords, encrypting/decrypting data, URLs encoding, using cryptographic algorithms. diff --git a/lib/internal/Magento/Framework/EntityManager/README.md b/lib/internal/Magento/Framework/EntityManager/README.md index f1c4692ef04d0..b6b71bd8c1bfd 100644 --- a/lib/internal/Magento/Framework/EntityManager/README.md +++ b/lib/internal/Magento/Framework/EntityManager/README.md @@ -1,16 +1,16 @@ # EntityManager **EntityManager** library contains functionality for entity persistence layer. -EntityManager supports persistence of basic entity attributes as well as extension and custom attributes +EntityManager supports persistence of basic entity attributes as well as extension and custom attributes added by 3rd party developers for the purpose of extending default entity behavior. It's not recommended to use EntityManager and its infrastructure for your entity persistence. -In the nearest future new Persistence Entity Manager would be released which will cover all the requirements for +In the nearest future new Persistence Entity Manager would be released which will cover all the requirements for persistence layer along with Query API as performance efficient APIs for Read scenarios. -Currently, it's recommended to use Resource Model infrastructure and make a successor of -Magento\Framework\Model\ResourceModel\Db\AbstractDb class or successor of +Currently, it's recommended to use Resource Model infrastructure and make a successor of +Magento\Framework\Model\ResourceModel\Db\AbstractDb class or successor of Magento\Eav\Model\Entity\AbstractEntity if EAV attributes support needed. -For filtering operations, it's recommended to use successor of +For filtering operations, it's recommended to use successor of Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection class. diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index f0aff097b10f5..9c249923197fb 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -17,6 +17,7 @@ class Escaper { /** * HTML special characters flag + * @var int */ private $htmlSpecialCharsFlag = ENT_QUOTES | ENT_SUBSTITUTE; @@ -96,7 +97,12 @@ function ($errorNumber, $errorString) { } ); $data = $this->prepareUnescapedCharacters($data); - $string = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); + $convmap = [0x80, 0x10FFFF, 0, 0x1FFFFF]; + $string = mb_encode_numericentity( + $data, + $convmap, + 'UTF-8' + ); try { $domDocument->loadHTML( '<html><body id="' . $wrapperElementId . '">' . $string . '</body></html>' @@ -113,7 +119,17 @@ function ($errorNumber, $errorString) { $this->escapeText($domDocument); $this->escapeAttributeValues($domDocument); - $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); + $result = mb_decode_numericentity( + // phpcs:ignore Magento2.Functions.DiscouragedFunction + html_entity_decode( + $domDocument->saveHTML(), + ENT_QUOTES|ENT_SUBSTITUTE, + 'UTF-8' + ), + $convmap, + 'UTF-8' + ); + preg_match('/<body id="' . $wrapperElementId . '">(.+)<\/body><\/html>$/si', $result, $matches); return !empty($matches) ? $matches[1] : ''; } else { @@ -346,6 +362,7 @@ public function escapeCss($string) * @param string $quote * @return string|array * @deprecated 101.0.0 + * @see MAGETWO-54971 */ public function escapeJsQuote($data, $quote = '\'') { @@ -366,6 +383,7 @@ public function escapeJsQuote($data, $quote = '\'') * @param string $data * @return string * @deprecated 101.0.0 + * @see MAGETWO-54971 */ public function escapeXssInUrl($data) { @@ -414,6 +432,7 @@ private function escapeScriptIdentifiers(string $data): string * @param bool $addSlashes * @return string * @deprecated 101.0.0 + * @see MAGETWO-54971 */ public function escapeQuote($data, $addSlashes = false) { @@ -428,6 +447,7 @@ public function escapeQuote($data, $addSlashes = false) * * @return \Magento\Framework\ZendEscaper * @deprecated 101.0.0 + * @see MAGETWO-54971 */ private function getEscaper() { @@ -443,6 +463,7 @@ private function getEscaper() * * @return \Psr\Log\LoggerInterface * @deprecated 101.0.0 + * @see MAGETWO-54971 */ private function getLogger() { diff --git a/lib/internal/Magento/Framework/Event/README.md b/lib/internal/Magento/Framework/Event/README.md index b79de1d8f6b0b..c045d82fadd35 100644 --- a/lib/internal/Magento/Framework/Event/README.md +++ b/lib/internal/Magento/Framework/Event/README.md @@ -5,5 +5,3 @@ * Event manager is responsible for event configuration processing and event dispatching. All client code that dispatches events must ask for \Magento\Framework\Event\Manager in constructor. * Event config provides interface to retrieve related observers configuration by specified event name. * Event observer object passes data from the code that fires the event to the observer function. There are two special types of observer objects supported in this library: Cron, and Regex. Each one has its own unique functionality in addition to basic observer functionality. - - diff --git a/lib/internal/Magento/Framework/Exception/README.md b/lib/internal/Magento/Framework/Exception/README.md index 4b0d4fbbaf421..c156a829d6adb 100644 --- a/lib/internal/Magento/Framework/Exception/README.md +++ b/lib/internal/Magento/Framework/Exception/README.md @@ -1,4 +1,4 @@ -#Exception +# Exception * Classes that extend the base Exception class represent types of Exceptions. * All Exception classes extend, directly or indirectly, LocalizedException. Thus, error messages can be localized. diff --git a/lib/internal/Magento/Framework/File/Pdf/Image.php b/lib/internal/Magento/Framework/File/Pdf/Image.php new file mode 100644 index 0000000000000..fe0ea6a4007dd --- /dev/null +++ b/lib/internal/Magento/Framework/File/Pdf/Image.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\File\Pdf; + +use Magento\Framework\File\Pdf\ImageResource\ImageFactory; + +class Image +{ + /** + * @var \Magento\Framework\File\Pdf\ImageResource\ImageFactory + */ + private ImageFactory $imageFactory; + + /** + * @param \Magento\Framework\File\Pdf\ImageResource\ImageFactory $imageFactory + */ + public function __construct(ImageFactory $imageFactory) + { + $this->imageFactory = $imageFactory; + } + + /** + * Filepath of image file + * + * @param string $filePath + * @return \Zend_Pdf_Resource_Image|\Zend_Pdf_Resource_Image_Jpeg|\Zend_Pdf_Resource_Image_Png|\Zend_Pdf_Resource_Image_Tiff|object + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Zend_Pdf_Exception + */ + public function imageWithPathAdvanced(string $filePath) + { + return $this->imageFactory->factory($filePath); + } +} diff --git a/lib/internal/Magento/Framework/File/Pdf/ImageResource/ImageFactory.php b/lib/internal/Magento/Framework/File/Pdf/ImageResource/ImageFactory.php new file mode 100644 index 0000000000000..6b0849afe3ad1 --- /dev/null +++ b/lib/internal/Magento/Framework/File/Pdf/ImageResource/ImageFactory.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\File\Pdf\ImageResource; + +use Exception; +use finfo; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Zend_Pdf_Exception; + +class ImageFactory +{ + /** + * @var \Magento\Framework\Filesystem + */ + private Filesystem $filesystem; + + /** + * @param \Magento\Framework\Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * New zend image factory instance + * + * @param string $filename + * @return \Zend_Pdf_Resource_Image_Jpeg|\Zend_Pdf_Resource_Image_Png|\Zend_Pdf_Resource_Image_Tiff|object + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Zend_Pdf_Exception + * @SuppressWarnings(PHPMD.LongVariable) + */ + public function factory(string $filename) + { + $mediaReader = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaReader->isFile($filename)) { + #require_once 'Zend/Pdf/Exception.php'; + throw new Zend_Pdf_Exception("Cannot create image resource. File not found."); + } + $tempFilenameFromBucketOrDisk = $this->createTemporaryFileAndPutContent($mediaReader, $filename); + $tempResourceFilePath = $this->getFilePathOfTemporaryFile($tempFilenameFromBucketOrDisk); + $typeOfImage = $this->getTypeOfImage($tempResourceFilePath, $filename); + $zendPdfImage = $this->getZendPdfImage($typeOfImage, $tempResourceFilePath); + $this->removeTemoraryFile($tempFilenameFromBucketOrDisk); + return $zendPdfImage; + } + + /** + * Create a temporary file and put content of the original file into it + * + * @param ReadInterface $mediaReader + * @param string $filename + * @return resource + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Zend_Pdf_Exception + * @SuppressWarnings(PHPMD.LongVariable) + */ + protected function createTemporaryFileAndPutContent(ReadInterface $mediaReader, string $filename) + { + $tempFilenameFromBucketOrDisk = tmpfile(); + if ($tempFilenameFromBucketOrDisk === false) { + #require_once 'Zend/Pdf/Exception.php'; + throw new Zend_Pdf_Exception('Cannot create temporary file'); + } + fwrite($tempFilenameFromBucketOrDisk, $mediaReader->readFile($filename)); + return $tempFilenameFromBucketOrDisk; + } + + /** + * Returns the path of the temporary file or nothing + * + * @param resource $tempFilenameFromBucketOrDisk + * @return string + * @SuppressWarnings(PHPMD.LongVariable) + */ + protected function getFilePathOfTemporaryFile($tempFilenameFromBucketOrDisk): string + { + try { + return stream_get_meta_data($tempFilenameFromBucketOrDisk)['uri']; + } catch (Exception $e) { + return ''; + } + } + + /** + * Get mime-type in safe way except internal errors + * + * @param string $filepath + * @param string $baseFileName + * @return mixed|string + * @throws \Zend_Pdf_Exception + */ + protected function getTypeOfImage(string $filepath, string $baseFileName) + { + if (class_exists('finfo', false) && !empty($filepath)) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $classicMimeType = $finfo->file($filepath); + } elseif (function_exists('mime_content_type') && !empty($filepath)) { + $classicMimeType = mime_content_type($filepath); + } else { + $classicMimeType = $this->fetchFallbackMimeType($baseFileName); + } + if (!empty($classicMimeType)) { + return explode("/", $classicMimeType)[1] ?? ''; + } else { + return ''; + } + } + + /** + * Fall back fetching of mimetype by original base file name + * + * @param string $baseFileName + * @return string + * @throws \Zend_Pdf_Exception + */ + protected function fetchFallbackMimeType(string $baseFileName): string + { + $extension = pathinfo($baseFileName, PATHINFO_EXTENSION); + switch (strtolower($extension)) { + case 'jpg': + //Fall through to next case; + case 'jpe': + //Fall through to next case; + case 'jpeg': + $classicMimeType = 'image/jpeg'; + break; + case 'png': + $classicMimeType = 'image/png'; + break; + case 'tif': + //Fall through to next case; + case 'tiff': + $classicMimeType = 'image/tiff'; + break; + default: + #require_once 'Zend/Pdf/Exception.php'; + throw new Zend_Pdf_Exception( + "Cannot create image resource. File extension not known or unsupported type." + ); + } + return $classicMimeType; + } + + /** + * Creates instance of Zend_Pdf_Resource_Image + * + * @param string $typeOfImage + * @param string $tempResourceFilePath + * @return \Zend_Pdf_Resource_Image_Jpeg|\Zend_Pdf_Resource_Image_Png|\Zend_Pdf_Resource_Image_Tiff|object + */ + protected function getZendPdfImage(string $typeOfImage, string $tempResourceFilePath) + { + $classToUseAsPdfImage = sprintf('Zend_Pdf_Resource_Image_%s', ucfirst($typeOfImage)); + return new $classToUseAsPdfImage($tempResourceFilePath); + } + + /** + * Removes the temporary file from disk + * + * @param resource $tempFilenameFromBucketOrDisk + * @return void + * @SuppressWarnings(PHPMD.LongVariable) + */ + protected function removeTemoraryFile($tempFilenameFromBucketOrDisk): void + { + fclose($tempFilenameFromBucketOrDisk); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php index d16fab37818b0..93d0f0b6affd7 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php @@ -23,8 +23,6 @@ class Read implements ReadInterface protected $path; /** - * File factory - * * @var \Magento\Framework\Filesystem\File\ReadFactory */ protected $fileFactory; @@ -307,4 +305,15 @@ public function isDirectory($path = null) return $this->driver->isDirectory($this->driver->getAbsolutePath($this->path, $path)); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return ['path' => $this->path]; + } } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index e455eaf8fc72f..9607b6a2f05f7 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -171,7 +171,7 @@ public function createSymlink($path, $destination, WriteInterface $targetDirecto $absolutePath = $this->driver->getAbsolutePath($this->path, $path); $absoluteDestination = $targetDirectory->getAbsolutePath($destination); - return $this->driver->symlink($absolutePath, $absoluteDestination, $targetDirectory->driver); + return $this->driver->symlink($absolutePath, $absoluteDestination, $this->driver); } /** diff --git a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php index 9d2280cd9adb5..c79a10e67d754 100644 --- a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php @@ -245,4 +245,15 @@ private function assertCode($code) ); } } + + /** + * Disable show ObjectManager internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 05251ecc25973..10b95caa09333 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -440,11 +440,12 @@ public function symlink($source, $destination, DriverInterface $targetDriver = n */ public function deleteFile($path) { - $result = @unlink($this->getScheme() . $path); + @unlink($this->getScheme() . $path); if ($this->stateful) { clearstatcache(true, $this->getScheme() . $path); } - if (!$result) { + + if ($this->isFile($path)) { throw new FileSystemException( new Phrase( 'The "%1" file can\'t be deleted. %2', @@ -452,7 +453,7 @@ public function deleteFile($path) ) ); } - return $result; + return true; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php index fdde17b2dabf8..9bf0f82ec4ff1 100644 --- a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php @@ -1,7 +1,5 @@ <?php declare(strict_types=1); /** - * Unit Test for \Magento\Framework\Filesystem\Directory\Write - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -15,6 +13,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Unit Test for \Magento\Framework\Filesystem\Directory\Write + */ class WriteTest extends TestCase { /** @@ -92,9 +93,7 @@ public function testIsWritable() public function testCreateSymlinkTargetDirectoryExists() { - $targetDir = $this->getMockBuilder(WriteInterface::class) - ->getMock(); - $targetDir->driver = $this->driver; + $targetDir = $this->getMockForAbstractClass(WriteInterface::class); $sourcePath = 'source/path/file'; $destinationDirectory = 'destination/path'; $destinationFile = $destinationDirectory . '/' . 'file'; @@ -117,7 +116,7 @@ public function testCreateSymlinkTargetDirectoryExists() ->with( $this->getAbsolutePath($sourcePath), $this->getAbsolutePath($destinationFile), - $targetDir->driver + $this->driver )->willReturn(true); $this->assertTrue($this->write->createSymlink($sourcePath, $destinationFile, $targetDir)); @@ -170,8 +169,6 @@ public function testRenameFile($sourcePath, $targetPath, $targetDir) { if ($targetDir !== null) { /** @noinspection PhpUndefinedFieldInspection */ - $targetDir->driver = $this->getMockBuilder(DriverInterface::class) - ->getMockForAbstractClass(); $targetDirPath = 'TARGET_PATH/'; $targetDir->expects($this->once()) ->method('getAbsolutePath') diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index ba4ba980d487f..8246c3dd0344c 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -16,6 +16,8 @@ use Magento\Framework\Filter\DirectiveProcessor\TemplateDirective; use Magento\Framework\Filter\DirectiveProcessor\VarDirective; use Magento\Framework\Stdlib\StringUtils; +use Magento\Framework\Filter\Template\SignatureProvider; +use Magento\Framework\Filter\Template\FilteringDepthMeter; /** * Template constructions filter @@ -98,17 +100,31 @@ class Template implements FilterInterface */ private $variableResolver; + /** + * @var SignatureProvider|null + */ + private $signatureProvider; + + /** + * @var FilteringDepthMeter|null + */ + private $filteringDepthMeter; + /** * @param StringUtils $string * @param array $variables * @param DirectiveProcessorInterface[] $directiveProcessors * @param VariableResolverInterface|null $variableResolver + * @param SignatureProvider|null $signatureProvider + * @param FilteringDepthMeter|null $filteringDepthMeter */ public function __construct( StringUtils $string, $variables = [], $directiveProcessors = [], - VariableResolverInterface $variableResolver = null + VariableResolverInterface $variableResolver = null, + SignatureProvider $signatureProvider = null, + FilteringDepthMeter $filteringDepthMeter = null ) { $this->string = $string; $this->setVariables($variables); @@ -116,6 +132,12 @@ public function __construct( $this->variableResolver = $variableResolver ?? ObjectManager::getInstance() ->get(VariableResolverInterface::class); + $this->signatureProvider = $signatureProvider ?? ObjectManager::getInstance() + ->get(SignatureProvider::class); + + $this->filteringDepthMeter = $filteringDepthMeter ?? ObjectManager::getInstance() + ->get(FilteringDepthMeter::class); + if (empty($directiveProcessors)) { $this->directiveProcessors = [ 'depend' => ObjectManager::getInstance()->get(DependDirective::class), @@ -167,6 +189,7 @@ public function getTemplateProcessor() * * @param string $value * @return string + * * @throws \Exception */ public function filter($value) @@ -178,6 +201,62 @@ public function filter($value) )->render()); } + $this->filteringDepthMeter->descend(); + + // Processing of template directives. + $templateDirectivesResults = array_unique( + $this->processDirectives($value), + SORT_REGULAR + ); + + $value = $this->applyDirectivesResults($value, $templateDirectivesResults); + + // Processing of deferred directives received from child templates + // or nested directives. + $deferredDirectivesResults = array_unique( + $this->processDirectives($value, true), + SORT_REGULAR + ); + + $value = $this->applyDirectivesResults($value, $deferredDirectivesResults); + + if ($this->filteringDepthMeter->showMark() > 1) { + // Signing own deferred directives (if any). + $signature = $this->signatureProvider->get(); + + foreach ($templateDirectivesResults as $result) { + if ($result['directive'] === $result['output']) { + $value = str_replace( + $result['output'], + $signature . $result['output'] . $signature, + $value + ); + } + } + } + + $value = $this->afterFilter($value); + + $this->filteringDepthMeter->ascend(); + + return $value; + } + + /** + * Processes template directives and returns an array that contains results produced by each directive. + * + * @param string $value + * @param bool $isSigned + * + * @return array + * + * @throws InvalidArgumentException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function processDirectives($value, $isSigned = false): array + { + $results = []; + foreach ($this->directiveProcessors as $directiveProcessor) { if (!$directiveProcessor instanceof DirectiveProcessorInterface) { throw new InvalidArgumentException( @@ -185,15 +264,100 @@ public function filter($value) ); } - if (preg_match_all($directiveProcessor->getRegularExpression(), $value, $constructions, PREG_SET_ORDER)) { + $pattern = $directiveProcessor->getRegularExpression(); + + if ($isSigned) { + $pattern = $this->embedSignatureIntoPattern($pattern); + } + + if (preg_match_all($pattern, $value, $constructions, PREG_SET_ORDER)) { foreach ($constructions as $construction) { $replacedValue = $directiveProcessor->process($construction, $this, $this->templateVars); - $value = str_replace($construction[0], $replacedValue, $value); + + $result = [ + 'directive' => $construction[0], + 'output' => $replacedValue + ]; + + if (count($this->afterFilterCallbacks) > 0) { + $result['callbacks'] = $this->afterFilterCallbacks; + + $this->resetAfterFilterCallbacks(); + } + + $results[] = $result; } } } - return $this->afterFilter($value); + return $results; + } + + /** + * Applies results produced by directives. + * + * @param string $value + * @param array $results + * + * @return string + */ + private function applyDirectivesResults(string $value, array $results): string + { + $processedResults = []; + + foreach ($results as $result) { + foreach ($processedResults as $processedResult) { + $result['directive'] = str_replace( + $processedResult['directive'], + $processedResult['output'], + $result['directive'] + ); + } + + $value = str_replace($result['directive'], $result['output'], $value); + + if (isset($result['callbacks'])) { + foreach ($result['callbacks'] as $callback) { + $this->addAfterFilterCallback($callback); + } + } + + $processedResults[] = $result; + } + + return $value; + } + + /** + * Modifies given regular expression pattern to be able to recognize signed directives. + * + * @param string $pattern + * + * @return string + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function embedSignatureIntoPattern(string $pattern): string + { + $signature = $this->signatureProvider->get(); + + $closingDelimiters = [ + '(' => ')', + '{' => '}', + '[' => ']', + '<' => '>' + ]; + + $closingDelimiter = $openingDelimiter = substr(trim($pattern), 0, 1); + + if (array_key_exists($openingDelimiter, $closingDelimiters)) { + $closingDelimiter = $closingDelimiters[$openingDelimiter]; + } + + $pattern = substr_replace($pattern, $signature, strpos($pattern, $openingDelimiter) + 1, 0); + $pattern = substr_replace($pattern, $signature, strrpos($pattern, $closingDelimiter), 0); + + return $pattern; } /** diff --git a/lib/internal/Magento/Framework/Filter/Template/FilteringDepthMeter.php b/lib/internal/Magento/Framework/Filter/Template/FilteringDepthMeter.php new file mode 100644 index 0000000000000..57257325be797 --- /dev/null +++ b/lib/internal/Magento/Framework/Filter/Template/FilteringDepthMeter.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter\Template; + +/** + * Meter of template filtering depth. + * + * Records and provides template/directive filtering depth (filtering recursion). + * Filtering depth 1 means that template or directive is root and has no parents. + */ +class FilteringDepthMeter +{ + /** + * @var int + */ + private $depth = 0; + + /** + * Increases filtering depth. + * + * @return void + */ + public function descend() + { + $this->depth++; + } + + /** + * Decreases filtering depth. + * + * @return void + */ + public function ascend() + { + $this->depth--; + } + + /** + * Shows current filtering depth. + * + * @return int + */ + public function showMark(): int + { + return $this->depth; + } +} diff --git a/lib/internal/Magento/Framework/Filter/Template/SignatureProvider.php b/lib/internal/Magento/Framework/Filter/Template/SignatureProvider.php new file mode 100644 index 0000000000000..3e476f3e5d79e --- /dev/null +++ b/lib/internal/Magento/Framework/Filter/Template/SignatureProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter\Template; + +/** + * Provider of a signature. + * + * Provides a signature which should be used to sign deferred directives + * (directives that should be processed in scope of a parent template + * instead of own scope, e.g. {{inlinecss}}). + */ +class SignatureProvider +{ + /** + * @var string|null + */ + private $signature; + + /** + * @var \Magento\Framework\Math\Random + */ + private $random; + + /** + * @param \Magento\Framework\Math\Random $random + */ + public function __construct( + \Magento\Framework\Math\Random $random + ) { + $this->random = $random; + } + + /** + * Generates a random string which will be used as a signature during runtime. + * + * @return string + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function get(): string + { + if ($this->signature === null) { + $this->signature = $this->random->getRandomString(32); + } + + return $this->signature; + } +} diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/Template/SignatureProviderTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/Template/SignatureProviderTest.php new file mode 100644 index 0000000000000..3cf685dc1ffd7 --- /dev/null +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/Template/SignatureProviderTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter\Test\Unit\Template; + +class SignatureProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Filter\Template\SignatureProvider + */ + protected $signatureProvider; + + /** + * @var \Magento\Framework\Math\Random|\PHPUnit\Framework\MockObject\MockObject + */ + protected $random; + + protected function setUp(): void + { + $this->random = $this->createPartialMock( + \Magento\Framework\Math\Random::class, + ['getRandomString'] + ); + + $this->signatureProvider = new \Magento\Framework\Filter\Template\SignatureProvider( + $this->random + ); + } + + public function testGet() + { + $expectedResult = 'Z0FFbeCU2R8bsVGJuTdkXyiiZBzsaceV'; + + $this->random->expects($this->once()) + ->method('getRandomString') + ->with(32) + ->willReturn($expectedResult); + + $this->assertEquals($expectedResult, $this->signatureProvider->get()); + + $this->random->expects($this->never()) + ->method('getRandomString'); + + $this->assertEquals($expectedResult, $this->signatureProvider->get()); + } +} diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index 973f7ac1d268f..dffb22c2b776f 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -28,11 +28,43 @@ class TemplateTest extends TestCase */ private $store; + /** + * @var \Magento\Framework\Filter\Template\SignatureProvider|\PHPUnit\Framework\MockObject\MockObject + */ + protected $signatureProvider; + + /** + * @var \Magento\Framework\Filter\Template\FilteringDepthMeter|\PHPUnit\Framework\MockObject\MockObject + */ + protected $filteringDepthMeter; + protected function setUp(): void { $objectManager = new ObjectManager($this); - $this->templateFilter = $objectManager->getObject(Template::class); + $this->store = $objectManager->getObject(Store::class); + + $this->signatureProvider = $this->createPartialMock( + \Magento\Framework\Filter\Template\SignatureProvider::class, + ['get'] + ); + + $this->signatureProvider->expects($this->any()) + ->method('get') + ->willReturn('Z0FFbeCU2R8bsVGJuTdkXyiiZBzsaceV'); + + $this->filteringDepthMeter = $this->createPartialMock( + \Magento\Framework\Filter\Template\FilteringDepthMeter::class, + ['showMark'] + ); + + $this->templateFilter = $objectManager->getObject( + \Magento\Framework\Filter\Template::class, + [ + 'signatureProvider' => $this->signatureProvider, + 'filteringDepthMeter' => $this->filteringDepthMeter + ] + ); } /** @@ -44,6 +76,10 @@ public function testAfterFilter() $value = 'test string'; $expectedResult = 'TEST STRING'; + $this->filteringDepthMeter->expects($this->any()) + ->method('showMark') + ->willReturn(1); + // Build arbitrary object to pass into the addAfterFilterCallback method $callbackObject = $this->getMockBuilder('stdObject') ->setMethods(['afterFilterCallbackMethod']) @@ -72,6 +108,10 @@ public function testAfterFilterCallbackReset() $value = 'test string'; $expectedResult = 'TEST STRING'; + $this->filteringDepthMeter->expects($this->any()) + ->method('showMark') + ->willReturn(1); + // Build arbitrary object to pass into the addAfterFilterCallback method $callbackObject = $this->getMockBuilder('stdObject') ->setMethods(['afterFilterCallbackMethod']) @@ -127,7 +167,7 @@ public function getTemplateAndExpectedResults($type) <ul> {{for in order.all_visible_items}} <li> - name: , lastname: , age: + name: , lastname: , age: </li> {{/for}} </ul> @@ -137,14 +177,14 @@ public function getTemplateAndExpectedResults($type) $template = <<<TEMPLATE <ul> {{for in order.all_visible_items}} - + {{/for}} </ul> TEMPLATE; $expected = <<<TEMPLATE <ul> {{for in order.all_visible_items}} - + {{/for}} </ul> TEMPLATE; @@ -153,14 +193,14 @@ public function getTemplateAndExpectedResults($type) $template = <<<TEMPLATE <ul> {{for in }} - + {{/for}} </ul> TEMPLATE; $expected = <<<TEMPLATE <ul> {{for in }} - + {{/for}} </ul> TEMPLATE; @@ -178,17 +218,17 @@ public function getTemplateAndExpectedResults($type) TEMPLATE; $expected = <<<TEMPLATE <ul> - + <li> index: 0 sku: ABC123 name: Product ABC price: 123 quantity: 2 </li> - + <li> index: 1 sku: DOREMI name: Product DOREMI price: 456 quantity: 1 </li> - + </ul> TEMPLATE; } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/ExceptionFormatter.php b/lib/internal/Magento/Framework/GraphQl/Exception/ExceptionFormatter.php index d94add74fd48c..a2ed744065656 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/ExceptionFormatter.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/ExceptionFormatter.php @@ -20,7 +20,7 @@ */ class ExceptionFormatter { - const HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS = 500; + public const HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS = 500; /** * @var State @@ -36,11 +36,11 @@ class ExceptionFormatter * @param State $appState * @param ErrorProcessor $errorProcessor * @param LoggerInterface $logger + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct(State $appState, ErrorProcessor $errorProcessor, LoggerInterface $logger) { $this->appState = $appState; - $errorProcessor->registerShutdownFunction(); $this->logger = $logger; } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php index 8275219e9e554..209170a27e117 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php @@ -8,18 +8,19 @@ namespace Magento\Framework\GraphQl\Exception; use GraphQL\Error\ClientAware; +use GraphQL\Error\ProvidesExtensions; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Phrase; /** * Exception for GraphQL to be thrown when data already exists */ -class GraphQlAlreadyExistsException extends AlreadyExistsException implements ClientAware +class GraphQlAlreadyExistsException extends AlreadyExistsException implements ClientAware, ProvidesExtensions { /** * Describing a category of the error */ - const EXCEPTION_CATEGORY = 'graphql-already-exists'; + public const EXCEPTION_CATEGORY = 'graphql-already-exists'; /** * @var boolean @@ -53,4 +54,15 @@ public function getCategory(): string { return self::EXCEPTION_CATEGORY; } + + /** + * Get error category + * + * @return array + */ + public function getExtensions(): array + { + $exceptionCategory['category'] = $this->getCategory(); + return $exceptionCategory; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php index 198e4ebc0aaae..a2eed01290d6c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php @@ -8,6 +8,7 @@ namespace Magento\Framework\GraphQl\Exception; use GraphQL\Error\ClientAware; +use GraphQL\Error\ProvidesExtensions; use Magento\Framework\Exception\AuthenticationException; use Magento\Framework\Phrase; @@ -16,12 +17,12 @@ * * @api */ -class GraphQlAuthenticationException extends AuthenticationException implements ClientAware +class GraphQlAuthenticationException extends AuthenticationException implements ClientAware, ProvidesExtensions { /** * Describing a category of the error */ - const EXCEPTION_CATEGORY = 'graphql-authentication'; + public const EXCEPTION_CATEGORY = 'graphql-authentication'; /** * @var boolean @@ -55,4 +56,15 @@ public function getCategory(): string { return self::EXCEPTION_CATEGORY; } + + /** + * Get error category + * + * @return array + */ + public function getExtensions(): array + { + $exceptionCategory['category'] = $this->getCategory(); + return $exceptionCategory; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php index 6f4ee736ebc47..b5cfaac8094ed 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php @@ -7,6 +7,8 @@ namespace Magento\Framework\GraphQl\Exception; +use GraphQL\Error\ClientAware; +use GraphQL\Error\ProvidesExtensions; use Magento\Framework\Phrase; use Magento\Framework\Exception\AuthorizationException; @@ -15,9 +17,9 @@ * * @api */ -class GraphQlAuthorizationException extends AuthorizationException implements \GraphQL\Error\ClientAware +class GraphQlAuthorizationException extends AuthorizationException implements ClientAware, ProvidesExtensions { - const EXCEPTION_CATEGORY = 'graphql-authorization'; + public const EXCEPTION_CATEGORY = 'graphql-authorization'; /** * @var boolean @@ -53,4 +55,15 @@ public function getCategory() : string { return self::EXCEPTION_CATEGORY; } + + /** + * Get error category + * + * @return array + */ + public function getExtensions(): array + { + $exceptionCategory['category'] = $this->getCategory(); + return $exceptionCategory; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php index 2c3f66f9e9765..89b9b1f48aac2 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php @@ -7,6 +7,7 @@ namespace Magento\Framework\GraphQl\Exception; +use GraphQL\Error\ProvidesExtensions; use Magento\Framework\Exception\AggregateExceptionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Phrase; @@ -17,9 +18,10 @@ * * @api */ -class GraphQlInputException extends LocalizedException implements AggregateExceptionInterface, ClientAware +// phpcs:disable Generic.Files.LineLength.TooLong +class GraphQlInputException extends LocalizedException implements AggregateExceptionInterface, ClientAware, ProvidesExtensions { - const EXCEPTION_CATEGORY = 'graphql-input'; + public const EXCEPTION_CATEGORY = 'graphql-input'; /** * @var boolean @@ -84,4 +86,15 @@ public function getErrors(): array { return $this->errors; } + + /** + * Get error category + * + * @return array + */ + public function getExtensions(): array + { + $exceptionCategory['category'] = $this->getCategory(); + return $exceptionCategory; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/BatchContractResolverWrapper.php b/lib/internal/Magento/Framework/GraphQl/Query/BatchContractResolverWrapper.php index f2f440a8a78d4..3623a0f941086 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/BatchContractResolverWrapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/BatchContractResolverWrapper.php @@ -12,12 +12,13 @@ use Magento\Framework\GraphQl\Query\Resolver\ResolveRequest; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; /** * Default logic to make batch contract resolvers work. */ -class BatchContractResolverWrapper implements ResolverInterface +class BatchContractResolverWrapper implements ResolverInterface, ResetAfterRequestInterface { /** * @var BatchServiceContractResolverInterface @@ -160,4 +161,12 @@ function () use ($i) { } ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->clearAggregated(); + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/BatchResolverWrapper.php b/lib/internal/Magento/Framework/GraphQl/Query/BatchResolverWrapper.php index 82938100c3d12..99499c3bf8a26 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/BatchResolverWrapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/BatchResolverWrapper.php @@ -11,14 +11,19 @@ use Magento\Framework\GraphQl\Query\Resolver\BatchRequestItemInterface; use Magento\Framework\GraphQl\Query\Resolver\BatchResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\BatchResponse; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\Resolver\ResolveRequest; +use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use RuntimeException; +use Throwable; /** * Wrapper containing batching logic for BatchResolverInterface. */ -class BatchResolverWrapper implements ResolverInterface +class BatchResolverWrapper implements ResolverInterface, ResetAfterRequestInterface { /** * @var BatchResolverInterface @@ -31,7 +36,7 @@ class BatchResolverWrapper implements ResolverInterface private $valueFactory; /** - * @var \Magento\Framework\GraphQl\Query\Resolver\ContextInterface|null + * @var ContextInterface|null */ private $context; @@ -78,14 +83,14 @@ private function clearAggregated(): void * Find resolved data for given request. * * @param BatchRequestItemInterface $item - * @throws \Throwable + * @throws Throwable * @return mixed */ private function findResolvedFor(BatchRequestItemInterface $item) { try { return $this->resolveFor($item); - } catch (\Throwable $exception) { + } catch (Throwable $exception) { $this->clearAggregated(); throw $exception; } @@ -95,13 +100,13 @@ private function findResolvedFor(BatchRequestItemInterface $item) * Resolve branch/leaf for given item. * * @param BatchRequestItemInterface $item - * @return mixed|\Magento\Framework\GraphQl\Query\Resolver\Value - * @throws \Throwable + * @return mixed|Value + * @throws Throwable */ private function resolveFor(BatchRequestItemInterface $item) { if (!$this->request) { - throw new \RuntimeException('Unknown batch request item'); + throw new RuntimeException('Unknown batch request item'); } if (!$this->response) { @@ -131,4 +136,12 @@ function () use ($item) { } ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->clearAggregated(); + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Fields.php b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php index 78062effe3d41..f0767cbb818e4 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Fields.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php @@ -7,34 +7,52 @@ namespace Magento\Framework\GraphQl\Query; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * This class holds a list of all queried fields and is used to enable performance optimization for schema loading. */ -class Fields +class Fields implements ResetAfterRequestInterface { /** * @var string[] */ private $fieldsUsedInQuery = []; + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Set Query for extracting list of fields. * - * @param string $query + * @param DocumentNode|string $query * @param array|null $variables * * @return void */ - public function setQuery($query, array $variables = null) + public function setQuery(DocumentNode|string $query, array $variables = null) { $queryFields = []; try { - $queryAst = \GraphQL\Language\Parser::parse(new \GraphQL\Language\Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } \GraphQL\Language\Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::NAME => function (Node $node) use (&$queryFields) { @@ -44,7 +62,7 @@ public function setQuery($query, array $variables = null) ] ); if (isset($variables)) { - $queryFields = array_merge($queryFields, $this->extractVariables($variables)); + $this->extractVariables($queryFields, $variables); } // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $e) { @@ -73,21 +91,26 @@ public function getFieldsUsedInQuery() /** * Extract and return list of all used fields in GraphQL query's variables * + * @param array $fields * @param array $variables * - * @return string[] + * @return void */ - private function extractVariables(array $variables): array + private function extractVariables(array &$fields, array $variables): void { - $fields = []; foreach ($variables as $key => $value) { if (is_array($value)) { - // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $fields = array_merge($fields, $this->extractVariables($value)); + $this->extractVariables($fields, $value); } $fields[$key] = $key; } + } - return $fields; + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->fieldsUsedInQuery = []; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php index 46e43ef6fe7ca..6038225bbd30c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php @@ -7,15 +7,14 @@ namespace Magento\Framework\GraphQl\Query; -use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryDepth; use GraphQL\Validator\Rules\QueryComplexity; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** @@ -26,6 +25,7 @@ * should be filtered and rejected. * * https://github.com/webonyx/graphql-php/blob/master/docs/security.md#query-complexity-analysis + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QueryComplexityLimiter { @@ -45,20 +45,49 @@ class QueryComplexityLimiter private $introspectionConfig; /** + * @var QueryParser + */ + private $queryParser; + + /** + * @var array + */ + private $rules = []; + + /** + * Constructor + * * @param int $queryDepth * @param int $queryComplexity * @param IntrospectionConfiguration $introspectionConfig + * @param QueryParser|null $queryParser */ public function __construct( int $queryDepth, int $queryComplexity, - IntrospectionConfiguration $introspectionConfig + IntrospectionConfiguration $introspectionConfig, + QueryParser $queryParser = null ) { $this->queryDepth = $queryDepth; $this->queryComplexity = $queryComplexity; $this->introspectionConfig = $introspectionConfig; + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } + /** + * Get rules + * + * @return array + */ + private function getRules() + { + if (empty($this->rules)) { + $this->rules[] = new QueryComplexity($this->queryComplexity); + $this->rules[] = new DisableIntrospection((int) $this->introspectionConfig->isIntrospectionDisabled()); + $this->rules[] = new QueryDepth($this->queryDepth); + } + return $this->rules; + } /** * Sets limits for query complexity * @@ -67,11 +96,9 @@ public function __construct( */ public function execute(): void { - DocumentValidator::addRule(new QueryComplexity($this->queryComplexity)); - DocumentValidator::addRule( - new DisableIntrospection((int) $this->introspectionConfig->isIntrospectionDisabled()) - ); - DocumentValidator::addRule(new QueryDepth($this->queryDepth)); + foreach ($this->getRules() as $rule) { + DocumentValidator::addRule($rule); + } } /** @@ -80,25 +107,28 @@ public function execute(): void * This is necessary for performance optimization, as extremely large queries require a substantial * amount of time to fully validate and can affect server performance. * - * @param string $query + * @param DocumentNode|string $query * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function validateFieldCount(string $query): void + public function validateFieldCount(DocumentNode|string $query): void { if (!empty($query)) { $totalFieldCount = 0; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } + Visitor::visit( - $queryAst, + $query, [ 'leave' => [ - NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) { + NodeKind::FIELD => function () use (&$totalFieldCount) { $totalFieldCount++; } ] ] ); - if ($totalFieldCount > $this->queryComplexity) { throw new GraphQlInputException(__( 'Max query complexity should be %1 but got %2.', diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryParser.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryParser.php new file mode 100644 index 0000000000000..0fe7ca1482f6b --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryParser.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Query; + +use GraphQL\Language\AST\DocumentNode; +use GraphQL\Language\Parser; +use GraphQL\Language\Source; + +/** + * Wrapper for GraphQl query parser. It parses query string into a `GraphQL\Language\AST\DocumentNode` + */ +class QueryParser +{ + /** + * @var string[] + */ + private $parsedQueries = []; + + /** + * Parse query string into a `GraphQL\Language\AST\DocumentNode`. + * + * @param string $query + * @return DocumentNode + * @throws \GraphQL\Error\SyntaxError + */ + public function parse(string $query): DocumentNode + { + $cacheKey = sha1($query); + if (!isset($this->parsedQueries[$cacheKey])) { + $this->parsedQueries[$cacheKey] = Parser::parse(new Source($query, 'GraphQL')); + } + return $this->parsedQueries[$cacheKey]; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php index 58449d6f23d06..8fb6cfabaa7b5 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php @@ -9,6 +9,8 @@ use GraphQL\Error\DebugFlag; use GraphQL\GraphQL; +use GraphQL\Language\AST\DocumentNode; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; @@ -16,6 +18,7 @@ /** * Wrapper for GraphQl execution of a schema + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QueryProcessor { @@ -34,27 +37,35 @@ class QueryProcessor */ private $errorHandler; + /** + * @var QueryParser + */ + private $queryParser; + /** * @param ExceptionFormatter $exceptionFormatter * @param QueryComplexityLimiter $queryComplexityLimiter * @param ErrorHandlerInterface $errorHandler + * @param QueryParser|null $queryParser * @SuppressWarnings(PHPMD.LongVariable) */ public function __construct( ExceptionFormatter $exceptionFormatter, QueryComplexityLimiter $queryComplexityLimiter, - ErrorHandlerInterface $errorHandler + ErrorHandlerInterface $errorHandler, + QueryParser $queryParser = null ) { $this->exceptionFormatter = $exceptionFormatter; $this->queryComplexityLimiter = $queryComplexityLimiter; $this->errorHandler = $errorHandler; + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } /** * Process a GraphQl query according to defined schema * * @param Schema $schema - * @param string $source + * @param DocumentNode|string $source * @param ContextInterface|null $contextValue * @param array|null $variableValues * @param string|null $operationName @@ -63,11 +74,14 @@ public function __construct( */ public function process( Schema $schema, - string $source, + DocumentNode|string $source, ContextInterface $contextValue = null, array $variableValues = null, string $operationName = null ): array { + if (is_string($source)) { + $source = $this->queryParser->parse($source); + } if (!$this->exceptionFormatter->shouldShowDetail()) { $this->queryComplexityLimiter->validateFieldCount($source); $this->queryComplexityLimiter->execute(); diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchResponse.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchResponse.php index da7548bb1f473..2c902b1321a99 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchResponse.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchResponse.php @@ -7,12 +7,14 @@ namespace Magento\Framework\GraphQl\Query\Resolver; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Contains responses for batch requests. * * @api */ -class BatchResponse +class BatchResponse implements ResetAfterRequestInterface { /** * @var \SplObjectStorage @@ -54,4 +56,12 @@ public function findResponseFor(BatchRequestItemInterface $item) return $this->responses[$item]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->responses = new \SplObjectStorage(); + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/BooleanType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/BooleanType.php index 15a6444999b2f..12f9047ea502a 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/BooleanType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/BooleanType.php @@ -15,5 +15,5 @@ class BooleanType extends \GraphQL\Type\Definition\BooleanType implements InputT /** * @var string */ - public $name = "Magento_Boolean"; + public string $name = "Magento_Boolean"; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php index beb4b5a311c94..c2a1de0061427 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php @@ -24,6 +24,11 @@ public function __construct(EnumElement $configElement) 'name' => $configElement->getName(), 'description' => $configElement->getDescription() ]; + + if (empty($configElement->getValues())) { + $config['values'] = []; + } + foreach ($configElement->getValues() as $value) { $config['values'][$value->getValue()] = [ 'value' => $value->getValue(), diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/FloatType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/FloatType.php index 8e314891c6a67..c761a77d35f40 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/FloatType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/FloatType.php @@ -15,5 +15,5 @@ class FloatType extends \GraphQL\Type\Definition\FloatType implements InputTypeI /** * @var string */ - public $name = "Magento_Float"; + public string $name = "Magento_Float"; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/IdType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/IdType.php index bf4a6af795a24..ef83eeeaa98a4 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/IdType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/IdType.php @@ -15,5 +15,5 @@ class IdType extends \GraphQL\Type\Definition\IDType implements InputTypeInterfa /** * @var string */ - public $name = "Magento_Id"; + public string $name = "Magento_Id"; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/IntType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/IntType.php index fc9dc078deeda..1195b63be3ff9 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/IntType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/IntType.php @@ -15,5 +15,5 @@ class IntType extends \GraphQL\Type\Definition\IntType implements InputTypeInter /** * @var string */ - public $name = "Magento_Int"; + public string $name = "Magento_Int"; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php index 553e8fe40efc2..62adb02d6f070 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php @@ -39,7 +39,7 @@ public function format(ConfigElementInterface $configElement, OutputTypeInterfac { $config = []; if ($configElement instanceof InterfaceType || $configElement instanceof UnionType) { - $typeResolver = $this->objectManager->create($configElement->getTypeResolver()); + $typeResolver = $this->objectManager->get($configElement->getTypeResolver()); $config['resolveType'] = function ($value) use ($typeResolver) { return $typeResolver->resolveType($value); }; diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/StringType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/StringType.php index 653d13b214bfb..623090cf39b2c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/StringType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/StringType.php @@ -15,5 +15,5 @@ class StringType extends \GraphQL\Type\Definition\StringType implements InputTyp /** * @var string */ - public $name = "Magento_String"; + public string $name = "Magento_String"; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php index cde8b6b3e446b..414e1eebe6531 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php @@ -10,13 +10,14 @@ use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\TypeInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; /** * GraphQL type object registry */ -class TypeRegistry +class TypeRegistry implements ResetAfterRequestInterface { /** * @var ObjectManagerInterface @@ -38,7 +39,7 @@ class TypeRegistry /** * @var TypeInterface[] */ - private $types; + private $types = []; /** * @param ObjectManagerInterface $objectManager @@ -92,4 +93,12 @@ public function get(string $typeName): TypeInterface } return $this->types[$typeName]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->types = []; + } } diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php index 217a233eae20c..e2084bfd2b266 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php @@ -7,6 +7,9 @@ namespace Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader; +use GraphQL\Type\Definition\Argument; +use GraphQL\Type\Definition\InputType; + /** * Reads fields and possible arguments from a meta field */ @@ -117,15 +120,15 @@ public function read(\GraphQL\Type\Definition\FieldDefinition $fieldMeta) : arra /** * Get the argumentMetaType result array * - * @param \GraphQL\Type\Definition\InputType $typeMeta - * @param \GraphQL\Type\Definition\FieldArgument $argumentMeta + * @param InputType $typeMeta + * @param Argument $argumentMeta * @param array $result * @return array */ private function argumentMetaType( - \GraphQL\Type\Definition\InputType $typeMeta, - \GraphQL\Type\Definition\FieldArgument $argumentMeta, - $result + InputType $typeMeta, + Argument $argumentMeta, + array $result ) : array { $argumentName = $argumentMeta->name; $result['arguments'][$argumentName] = array_merge( diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/TypeMetaWrapperReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/TypeMetaWrapperReader.php index 78e3f28763385..4086c4f641503 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/TypeMetaWrapperReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/TypeMetaWrapperReader.php @@ -7,36 +7,41 @@ namespace Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader; +use GraphQL\Type\Definition\ListOfType; +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ScalarType; +use GraphQL\Type\Definition\Type; + /** * Common cases for types that need extra formatting like wrapping or additional properties added to their definition */ class TypeMetaWrapperReader { - const ARGUMENT_PARAMETER = 'Argument'; + public const ARGUMENT_PARAMETER = 'Argument'; - const OUTPUT_FIELD_PARAMETER = 'OutputField'; + public const OUTPUT_FIELD_PARAMETER = 'OutputField'; - const INPUT_FIELD_PARAMETER = 'InputField'; + public const INPUT_FIELD_PARAMETER = 'InputField'; /** * Read from type meta data and determine wrapping types that are needed and extra properties that need to be added * - * @param \GraphQL\Type\Definition\Type $meta + * @param Type $meta * @param string $parameterType Argument|OutputField|InputField * @return array */ - public function read(\GraphQL\Type\Definition\Type $meta, string $parameterType) : array + public function read(Type $meta, string $parameterType) : array { $result = []; - if ($meta instanceof \GraphQL\Type\Definition\NonNull) { + if ($meta instanceof NonNull) { $result['required'] = true; $meta = $meta->getWrappedType(); } else { $result['required'] = false; } - if ($meta instanceof \GraphQL\Type\Definition\ListOfType) { - $itemTypeMeta = $meta->ofType; - if ($itemTypeMeta instanceof \GraphQL\Type\Definition\NonNull) { + if ($meta instanceof ListOfType) { + $itemTypeMeta = $meta->getWrappedType(); + if ($itemTypeMeta instanceof NonNull) { $result['itemsRequired'] = true; $itemTypeMeta = $itemTypeMeta->getWrappedType(); } else { @@ -44,7 +49,7 @@ public function read(\GraphQL\Type\Definition\Type $meta, string $parameterType) } $itemTypeName = $itemTypeMeta->name; $result['itemType'] = $itemTypeName; - if ($itemTypeMeta instanceof \GraphQL\Type\Definition\ScalarType) { + if ($itemTypeMeta instanceof ScalarType) { $result['type'] = 'ScalarArray' . $parameterType; } else { $result['type'] = 'ObjectArray' . $parameterType; diff --git a/lib/internal/Magento/Framework/HTTP/LaminasClient.php b/lib/internal/Magento/Framework/HTTP/LaminasClient.php index 10bb4dd18c5ce..082b0b1ceb89b 100644 --- a/lib/internal/Magento/Framework/HTTP/LaminasClient.php +++ b/lib/internal/Magento/Framework/HTTP/LaminasClient.php @@ -12,9 +12,10 @@ use Laminas\Http\Client; use Magento\Framework\HTTP\Adapter\Curl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Traversable; -class LaminasClient extends Client +class LaminasClient extends Client implements ResetAfterRequestInterface { /** * Internal flag to allow decoding of request body @@ -37,6 +38,14 @@ public function __construct($uri = null, $options = null) parent::__construct($uri, $options); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->reset(); + } + /** * Change value of internal flag to disable/enable custom prepare functionality * diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index d5676fbe1e2e6..27fc86b94e8b4 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -5,14 +5,14 @@ */ namespace Magento\Framework\HTTP\PhpEnvironment; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; -use Magento\Framework\Stdlib\StringUtils; use Laminas\Http\Header\HeaderInterface; use Laminas\Stdlib\Parameters; use Laminas\Stdlib\ParametersInterface; use Laminas\Uri\UriFactory; use Laminas\Uri\UriInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; +use Magento\Framework\Stdlib\StringUtils; /** * HTTP Request for current PHP environment. @@ -451,6 +451,7 @@ private function getSslOffloadHeader() * * @return \Magento\Framework\App\Config * @deprecated 100.1.0 + * @see Nothing */ private function getAppConfig() { @@ -820,4 +821,20 @@ public function setForwarded($forwarded) $this->forwarded = $forwarded; return $this; } + + /** + * Retrieve debug info + * + * @return array + */ + public function __debugInfo() + { + return [ + 'pathInfo' => $this->pathInfo, + 'requestString' => $this->requestString, + 'module' => $this->module, + 'controller' => $this->controller, + 'action' => $this->action, + ]; + } } diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php index 2360804a595c0..b51cb2c7cc511 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php @@ -7,17 +7,20 @@ namespace Magento\Framework\HTTP\PhpEnvironment; +use Magento\Framework\App\Response\HttpInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Base HTTP response object */ -class Response extends \Laminas\Http\PhpEnvironment\Response implements \Magento\Framework\App\Response\HttpInterface +class Response extends \Laminas\Http\PhpEnvironment\Response implements HttpInterface, ResetAfterRequestInterface { /** * Flag; is this response a redirect? * * @var boolean */ - protected $isRedirect = false; + protected bool $isRedirect = false; /** * @inheritdoc @@ -191,4 +194,18 @@ public function __sleep() { return ['content', 'isRedirect', 'statusCode']; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->metadata = []; + $this->content = null; + $this->headers = null; + $this->contentSent = false; + $this->isRedirect = false; + $this->statusCode = 200; + $this->reasonPhrase = null; + } } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index 037a2eb56d6c1..5c06eb6390c0b 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -343,9 +343,14 @@ private function applyTransparency(&$imageResourceTo, $transparentIndex): void // fill image with indexed non-alpha transparency $transparentColor = false; - if ($transparentIndex >= 0 && $transparentIndex <= imagecolorstotal($this->_imageHandler)) { - list($red, $green, $blue) = array_values(imagecolorsforindex($this->_imageHandler, $transparentIndex)); - $transparentColor = imagecolorallocate($imageResourceTo, (int) $red, (int) $green, (int) $blue); + if ($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($this->_imageHandler)) { + try { + $colorsForIndex = imagecolorsforindex($this->_imageHandler, $transparentIndex); + list($red, $green, $blue) = array_values($colorsForIndex); + $transparentColor = imagecolorallocate($imageResourceTo, (int) $red, (int) $green, (int) $blue); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (\ValueError $e) { + } } if (false === $transparentColor) { throw new \InvalidArgumentException('Failed to allocate transparent color for image.'); @@ -387,7 +392,7 @@ private function _getTransparency($imageResource, $fileType, &$isAlpha = false, if (IMAGETYPE_GIF === $fileType || IMAGETYPE_PNG === $fileType) { // check for specific transparent color $transparentIndex = imagecolortransparent($imageResource); - if ($transparentIndex >= 0) { + if ($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($imageResource)) { return $transparentIndex; } elseif (IMAGETYPE_PNG === $fileType) { // assume that truecolor PNG has transparency diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php index f04bea1030ade..1d0417ad23aca 100644 --- a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php @@ -92,7 +92,7 @@ protected function _getClassMethods() protected function isInterceptedMethod(\ReflectionMethod $method) { return !($method->isConstructor() || $method->isFinal() || $method->isStatic() || $method->isDestructor()) && - !in_array($method->getName(), ['__sleep', '__wakeup', '__clone']); + !in_array($method->getName(), ['__sleep', '__wakeup', '__clone', '_resetState']); } /** diff --git a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php index 26697e70a8f87..67c53e40b595e 100644 --- a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php +++ b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php @@ -292,4 +292,15 @@ public function merge(array $config) { $this->_data = $this->pluginListGenerator->merge($config, $this->_data); } + + /** + * Disable show PluginList internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Locale/README.md b/lib/internal/Magento/Framework/Locale/README.md index 0785a09cd518d..ad02c374f42a5 100644 --- a/lib/internal/Magento/Framework/Locale/README.md +++ b/lib/internal/Magento/Framework/Locale/README.md @@ -1,4 +1,5 @@ -#Locale +# Locale + * Contains logic to handle currency and number formatting based on locale. * Provides access to locale information translated into given languages. * Manages locale information for a store instance. diff --git a/lib/internal/Magento/Framework/Locale/Resolver.php b/lib/internal/Magento/Framework/Locale/Resolver.php index 55ef2a4e9a30c..a35603c322712 100644 --- a/lib/internal/Magento/Framework/Locale/Resolver.php +++ b/lib/internal/Magento/Framework/Locale/Resolver.php @@ -8,16 +8,17 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Manages locale config information. */ -class Resolver implements ResolverInterface +class Resolver implements ResolverInterface, ResetAfterRequestInterface { /** * Resolver default locale */ - const DEFAULT_LOCALE = 'en_US'; + public const DEFAULT_LOCALE = 'en_US'; /** * Default locale code @@ -27,8 +28,6 @@ class Resolver implements ResolverInterface protected $defaultLocale; /** - * Scope type - * * @var string */ protected $scopeType; @@ -175,4 +174,13 @@ public function revert() } return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->locale = null; + $this->emulatedLocales = []; + } } diff --git a/lib/internal/Magento/Framework/Lock/README.md b/lib/internal/Magento/Framework/Lock/README.md index cd5ae425b949d..75078d6a37895 100644 --- a/lib/internal/Magento/Framework/Lock/README.md +++ b/lib/internal/Magento/Framework/Lock/README.md @@ -3,6 +3,7 @@ Lock library provides mechanism to acquire Magento system-wide lock. Default implementation is based on MySQL locks, where any locks are automatically released on connection close. The library provides interface *LockManagerInterface* which provides following methods: + * *lock* - Acquires a named lock * *unlock* - Releases a named lock * *isLocked* - Tests if a named lock exists diff --git a/lib/internal/Magento/Framework/Logger/Handler/Base.php b/lib/internal/Magento/Framework/Logger/Handler/Base.php index 44c4af7ffc194..faca85cb44866 100644 --- a/lib/internal/Magento/Framework/Logger/Handler/Base.php +++ b/lib/internal/Magento/Framework/Logger/Handler/Base.php @@ -88,4 +88,14 @@ protected function write(array $record): void parent::write($record); } + + /** + * Retrieve debug info + * + * @return string[] + */ + public function __debugInfo() + { + return ['fileName' => $this->fileName]; + } } diff --git a/lib/internal/Magento/Framework/Math/README.md b/lib/internal/Magento/Framework/Math/README.md index d7b31ad376563..7242beea699a7 100644 --- a/lib/internal/Magento/Framework/Math/README.md +++ b/lib/internal/Magento/Framework/Math/README.md @@ -1 +1 @@ -The Math module provides mathematical functionality including price rounding, finding floating point remainder of a division, random number and string generation, and unique hash generation. \ No newline at end of file +The Math module provides mathematical functionality including price rounding, finding floating point remainder of a division, random number and string generation, and unique hash generation. diff --git a/lib/internal/Magento/Framework/Message/README.md b/lib/internal/Magento/Framework/Message/README.md index 40ae8358e8b19..b3c402e9614e0 100644 --- a/lib/internal/Magento/Framework/Message/README.md +++ b/lib/internal/Magento/Framework/Message/README.md @@ -1,3 +1,3 @@ # Message -**Message** library is responsible for message creation and management. \ No newline at end of file +**Message** library is responsible for message creation and management. diff --git a/lib/internal/Magento/Framework/MessageQueue/CountableQueueInterface.php b/lib/internal/Magento/Framework/MessageQueue/CountableQueueInterface.php new file mode 100644 index 0000000000000..a6aec418ab5d8 --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/CountableQueueInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\MessageQueue; + +use Countable; + +/** + * {@inheritdoc} + * + * Queue driver that implements this interface must implement count() method + * that returns the number of pending messages in the queue + */ +interface CountableQueueInterface extends QueueInterface, Countable +{ + /** + * Get number of pending messages in the queue + * + * @return int + */ + public function count(): int; +} diff --git a/lib/internal/Magento/Framework/MessageQueue/README.md b/lib/internal/Magento/Framework/MessageQueue/README.md index 9708d30102f43..4e0de76b13ecc 100644 --- a/lib/internal/Magento/Framework/MessageQueue/README.md +++ b/lib/internal/Magento/Framework/MessageQueue/README.md @@ -1 +1 @@ -This component is designed to provide Message Queue Framework +This component is designed to provide Message Queue Framework diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 306159f8d22d5..c5ea050fc60c7 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -92,6 +92,20 @@ private function flattenCustomAttributesArrayToMap(array $customAttributesData): return array_reduce( $customAttributesData, function (array $acc, array $customAttribute): array { + if (!isset($customAttribute['value']) + && isset($customAttribute['selected_options']) + && is_array($customAttribute['selected_options']) + ) { + $customAttribute['value'] = implode( + ',', + array_map( + function (array $option): string { + return (string)$option['value']; + }, + $customAttribute['selected_options'] + ) + ); + } $acc[$customAttribute['attribute_code']] = $customAttribute['value']; return $acc; }, @@ -113,7 +127,7 @@ protected function filterCustomAttributes($data) if (isset($data[self::CUSTOM_ATTRIBUTES][0])) { $data[self::CUSTOM_ATTRIBUTES] = $this->flattenCustomAttributesArrayToMap($data[self::CUSTOM_ATTRIBUTES]); } - $customAttributesCodes = $this->getCustomAttributesCodes(); + $customAttributesCodes = $this->getCustomAttributesCodes(); $data[self::CUSTOM_ATTRIBUTES] = array_intersect_key( (array) $data[self::CUSTOM_ATTRIBUTES], array_flip($customAttributesCodes) diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index e45709ab63882..227f45340dc4e 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -8,6 +8,7 @@ use Laminas\Validator\ValidatorChain; use Laminas\Validator\ValidatorInterface; use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Phrase; /** @@ -511,7 +512,7 @@ public function getResourceCollection() new \Magento\Framework\Phrase('Model collection resource name is not defined.') ); } - return $this->_resourceCollection ? clone $this + return !$this->_collectionName ? clone $this ->_resourceCollection : \Magento\Framework\App\ObjectManager::getInstance() ->create( $this->_collectionName @@ -754,6 +755,7 @@ protected function _getValidatorBeforeSave() * Returns FALSE, if no validation rules exist. * * @return ValidatorInterface|bool + * @throws LocalizedException */ protected function _createValidatorBeforeSave() { @@ -763,19 +765,27 @@ protected function _createValidatorBeforeSave() return false; } - if ($modelRules && $resourceRules) { - $validator = new ValidatorChain(); - $validator->addValidator($modelRules); - $validator->addValidator($resourceRules); - } elseif ($modelRules) { - $validator = $modelRules; - } else { - $validator = $resourceRules; + $validator = $this->getValidator(); + if ($modelRules) { + $validator->attach($modelRules); + } + if ($resourceRules) { + $validator->attach($resourceRules); } return $validator; } + /** + * Create validator instance + * + * @return ValidatorChain + */ + private function getValidator(): ValidatorChain + { + return \Magento\Framework\App\ObjectManager::getInstance()->create(ValidatorChain::class); + } + /** * Template method to return validate rules for the entity * diff --git a/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php b/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php index e6b492b0a04fc..3e508b8e14fb0 100644 --- a/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php +++ b/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php @@ -1,7 +1,5 @@ <?php /** - * Action validator, remove action - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -45,7 +43,6 @@ public function __construct(\Magento\Framework\Registry $registry, array $protec public function isAllowed(AbstractModel $model) { $isAllowed = true; - if ($this->registry->registry('isSecureArea')) { $isAllowed = true; } elseif (in_array($this->getBaseClassName($model), $this->protectedModels)) { @@ -57,6 +54,7 @@ public function isAllowed(AbstractModel $model) /** * Get clean model name without Interceptor and Proxy part and slashes + * * @param object $object * @return mixed */ diff --git a/lib/internal/Magento/Framework/Model/README.md b/lib/internal/Magento/Framework/Model/README.md index 0f5eb1761169f..288902765be65 100644 --- a/lib/internal/Magento/Framework/Model/README.md +++ b/lib/internal/Magento/Framework/Model/README.md @@ -1,3 +1,3 @@ # Model -**Model** library provides support for MVC models, model context, and storing and retrieving models to the database. \ No newline at end of file +**Model** library provides support for MVC models, model context, and storing and retrieving models to the database. 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 4b8c651dfaa1c..4e335e62d8f1a 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php @@ -145,6 +145,27 @@ protected function _construct() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_model = null; + $this->_resourceModel = null; + $this->_fieldsToSelect = null; + $this->expressionFieldsToSelect = []; + $this->_initialFieldsToSelect = null; + $this->_fieldsToSelectChanged = false; + $this->_joinedTables = []; + $this->_mainTable = null; + $this->_resetItemsDataChanged = false; + $this->_eventPrefix = ''; + $this->_eventObject = ''; + $this->_construct(); + $this->_initSelect(); + } + /** * Retrieve main table * diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php index a287fa5e1af42..0040afa829896 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php @@ -5,10 +5,12 @@ */ namespace Magento\Framework\Model\ResourceModel\Db\VersionControl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Class Snapshot register snapshot of entity data, for tracking changes */ -class Snapshot +class Snapshot implements ResetAfterRequestInterface { /** * Array of snapshots of entities data @@ -86,4 +88,12 @@ public function clear(\Magento\Framework\DataObject $entity = null) $this->snapshotData = []; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->snapshotData = []; + } } diff --git a/lib/internal/Magento/Framework/Module/ModuleList.php b/lib/internal/Magento/Framework/Module/ModuleList.php index b3cf433bbaf45..b458b60b9caba 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList.php +++ b/lib/internal/Magento/Framework/Module/ModuleList.php @@ -140,8 +140,23 @@ public function isModuleInfoAvailable() */ private function loadConfigData() { - if (null === $this->configData && null !== $this->config->get(ConfigOptionsListConstants::KEY_MODULES)) { - $this->configData = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); + if (null === $this->configData) { + $config = $this->config->get(ConfigOptionsListConstants::KEY_MODULES); + if (null !== $config) { + $this->configData = $config; + } } } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * + * @return array|null + */ + public function __debugInfo(): ?array + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Module/README.md b/lib/internal/Magento/Framework/Module/README.md index 533097f5f273b..59a50e6186e92 100644 --- a/lib/internal/Magento/Framework/Module/README.md +++ b/lib/internal/Magento/Framework/Module/README.md @@ -6,4 +6,4 @@ Magento\Framework\Module is Magento framework component that allows to build mod * module manager that provides all information about loaded modules * directory reader, that allows to read configuration files from module * ability to turn on/off module output in separate configuration application - * module db data installers \ No newline at end of file + * module db data installers diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleListTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleListTest.php index 07d35c77026eb..c532af8580ca6 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleListTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleListTest.php @@ -129,7 +129,7 @@ public function testIsModuleInfoAvailableNoConfig(): void { $this->config ->method('get') - ->willReturnOnConsecutiveCalls(['modules' => 'testModule'], null); + ->willReturnOnConsecutiveCalls(null); $this->assertFalse($this->model->isModuleInfoAvailable()); } @@ -144,7 +144,7 @@ public function testIsModuleInfoAvailableNoConfig(): void private function setLoadConfigExpectation($isExpected = true): void { if ($isExpected) { - $this->config->expects($this->exactly(2))->method('get')->willReturn(self::$enabledFixture); + $this->config->expects($this->once())->method('get')->willReturn(self::$enabledFixture); } else { $this->config->expects($this->never())->method('get'); } diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/Setup/MigrationTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/Setup/MigrationTest.php index 1dece5f8183cb..7f763d5e2ba59 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/Setup/MigrationTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/Setup/MigrationTest.php @@ -203,7 +203,7 @@ public function testAppendClassAliasReplace() */ public function testDoUpdateClassAliases($replaceRules, $tableData, $expected, $aliasesMap = []) { - $this->markTestIncomplete('Requires refactoring of class that is tested, covers to many methods'); + $this->markTestSkipped('Requires refactoring of class that is tested, covers to many methods'); $this->_actualUpdateResult = []; $tableRowsCount = count($tableData); diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php index f3ed80330e021..ed5bde9840117 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php @@ -18,6 +18,7 @@ use Magento\Framework\Mview\View\CollectionInterface; use Magento\Framework\Mview\View\StateInterface; use Magento\Framework\Mview\View\Subscription; +use Magento\Framework\Mview\View\SubscriptionStatementPostprocessorInterface; use Magento\Framework\Mview\ViewInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -127,6 +128,9 @@ protected function setUp(): void ] ] ]); + $statementPostprocessorMock = $this->createMock(SubscriptionStatementPostprocessorInterface::class); + $statementPostprocessorMock->method('process') + ->willReturnArgument(2); $this->model = new Subscription( $this->resourceMock, $this->triggerFactoryMock, @@ -136,7 +140,8 @@ protected function setUp(): void 'columnName', [], [], - $mviewConfigMock + $mviewConfigMock, + $statementPostprocessorMock ); } @@ -417,6 +422,9 @@ public function testBuildStatementIgnoredColumnSubscriptionLevel(): void ] ] ]); + $statementPostprocessorMock = $this->createMock(SubscriptionStatementPostprocessorInterface::class); + $statementPostprocessorMock->method('process') + ->willReturnArgument(2); $this->connectionMock->expects($this->any()) ->method('isTableExists') @@ -464,7 +472,8 @@ public function testBuildStatementIgnoredColumnSubscriptionLevel(): void 'columnName', [], $ignoredData, - $mviewConfigMock + $mviewConfigMock, + $statementPostprocessorMock ); $method = new ReflectionMethod($model, 'buildStatement'); diff --git a/lib/internal/Magento/Framework/Mview/View/CompositeSubscriptionStatementPostprocessor.php b/lib/internal/Magento/Framework/Mview/View/CompositeSubscriptionStatementPostprocessor.php new file mode 100644 index 0000000000000..ba6fdbdd2b220 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/CompositeSubscriptionStatementPostprocessor.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mview\View; + +class CompositeSubscriptionStatementPostprocessor implements SubscriptionStatementPostprocessorInterface +{ + /** + * @var SubscriptionStatementPostprocessorInterface[] + */ + private $postprocessors; + + /** + * @param SubscriptionStatementPostprocessorInterface[] $postprocessors + */ + public function __construct(array $postprocessors = []) + { + $this->postprocessors = $postprocessors; + } + + /** + * @inheritdoc + */ + public function process(string $tableName, string $event, string $statement): string + { + foreach ($this->postprocessors as $postprocessor) { + $statement = $postprocessor->process($tableName, $event, $statement); + } + + return $statement; + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/Subscription.php b/lib/internal/Magento/Framework/Mview/View/Subscription.php index 933d075b35f75..4c96495aba767 100644 --- a/lib/internal/Magento/Framework/Mview/View/Subscription.php +++ b/lib/internal/Magento/Framework/Mview/View/Subscription.php @@ -87,6 +87,11 @@ class Subscription implements SubscriptionInterface, SubscriptionTriggersInterfa */ private $mviewConfig; + /** + * @var SubscriptionStatementPostprocessorInterface + */ + private $statementPostprocessor; + /** * @var Trigger[] */ @@ -102,6 +107,8 @@ class Subscription implements SubscriptionInterface, SubscriptionTriggersInterfa * @param array $ignoredUpdateColumns * @param array $ignoredUpdateColumnsBySubscription * @param Config|null $mviewConfig + * @param SubscriptionStatementPostprocessorInterface|null $statementPostprocessor + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourceConnection $resource, @@ -112,7 +119,8 @@ public function __construct( $columnName, $ignoredUpdateColumns = [], $ignoredUpdateColumnsBySubscription = [], - Config $mviewConfig = null + ?Config $mviewConfig = null, + ?SubscriptionStatementPostprocessorInterface $statementPostprocessor = null ) { $this->connection = $resource->getConnection(); $this->triggerFactory = $triggerFactory; @@ -124,6 +132,8 @@ public function __construct( $this->ignoredUpdateColumns = $ignoredUpdateColumns; $this->ignoredUpdateColumnsBySubscription = $ignoredUpdateColumnsBySubscription; $this->mviewConfig = $mviewConfig ?? ObjectManager::getInstance()->get(Config::class); + $this->statementPostprocessor = $statementPostprocessor + ?? ObjectManager::getInstance()->get(SubscriptionStatementPostprocessorInterface::class); } /** @@ -324,13 +334,16 @@ protected function buildStatement(string $event, ViewInterface $view): string } $columns = $this->prepareColumns($view, $event); - return sprintf( + $statement = sprintf( $trigger, $this->getProcessor()->getPreStatements(), $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), implode(', ', $columns['column_names']), implode(', ', $columns['column_values']) ); + $statement = $this->statementPostprocessor->process($this->getTableName(), $event, $statement); + + return $statement; } /** diff --git a/lib/internal/Magento/Framework/Mview/View/SubscriptionStatementPostprocessorInterface.php b/lib/internal/Magento/Framework/Mview/View/SubscriptionStatementPostprocessorInterface.php new file mode 100644 index 0000000000000..288eb16f0222d --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/SubscriptionStatementPostprocessorInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mview\View; + +interface SubscriptionStatementPostprocessorInterface +{ + /** + * Postprocess subscription statement. + * + * @param string $tableName + * @param string $event + * @param string $statement + * @return string + */ + public function process(string $tableName, string $event, string $statement): string; +} diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php index e1ae712c2af1a..8bb78ba303e94 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php @@ -98,10 +98,18 @@ protected function _getClassMethods() ]; $methods[] = [ 'name' => '__clone', - 'body' => "\$this->_subject = clone \$this->_getSubject();", + 'body' => "if (\$this->_subject) {\n" . + " \$this->_subject = clone \$this->_getSubject();\n" . + "}\n", 'docblock' => ['shortDescription' => 'Clone proxied instance'], ]; + $methods[] = [ + 'name' => '__debugInfo', + 'body' => "return ['i' => \$this->_subject];", + 'docblock' => ['shortDescription' => 'Debug proxied instance'], + ]; + $methods[] = [ 'name' => '_getSubject', 'visibility' => 'protected', @@ -127,11 +135,21 @@ protected function _getClassMethods() ) && !in_array( $method->getName(), - ['__sleep', '__wakeup', '__clone'] + ['__sleep', '__wakeup', '__clone', '__debugInfo', '_resetState'] ) ) { $methods[] = $this->_getMethodInfo($method); } + if ($method->getName() === '_resetState') { + $methods[] = [ + 'name' => '_resetState', + 'returnType' => 'void', + 'body' => "if (\$this->_subject) {\n" . + " \$this->_subject->_resetState(); \n" . + "}\n", + 'docblock' => ['shortDescription' => 'Reset state of proxied instance'], + ]; + } } return $methods; diff --git a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php index ba0551376421c..dd7df3a3af535 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php +++ b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php @@ -354,6 +354,7 @@ public function getPreferences() * * @return SerializerInterface * @deprecated 101.0.0 + * @see Nothing */ private function getSerializer() { @@ -363,4 +364,15 @@ private function getSerializer() } return $this->serializer; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Definition/Runtime.php b/lib/internal/Magento/Framework/ObjectManager/Definition/Runtime.php index 6663732ec8d33..536911db62d40 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Definition/Runtime.php +++ b/lib/internal/Magento/Framework/ObjectManager/Definition/Runtime.php @@ -1,16 +1,12 @@ <?php /** - * Runtime class definitions. \Reflection is used to parse constructor signatures. Should be used only in dev mode. - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\ObjectManager\Definition; /** - * Class Runtime - * - * @package Magento\Framework\ObjectManager\Definition + * Runtime class definitions. \Reflection is used to parse constructor signatures. Should be used only in dev mode. */ class Runtime implements \Magento\Framework\ObjectManager\DefinitionInterface { @@ -65,4 +61,15 @@ public function getClasses() { return []; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php b/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php index 08a3f9939d851..452750937c1e1 100644 --- a/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php +++ b/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php @@ -1,4 +1,10 @@ <?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\ObjectManager; + /** * Magento object manager. Responsible for instantiating objects taking into account: * - constructor arguments (using configured, and provided parameters) @@ -7,11 +13,7 @@ * * Intentionally contains multiple concerns for best performance * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. */ -namespace Magento\Framework\ObjectManager; - class ObjectManager implements \Magento\Framework\ObjectManagerInterface { /** @@ -32,6 +34,7 @@ class ObjectManager implements \Magento\Framework\ObjectManagerInterface protected $_config; /** + * phpcs:disable Magento2.Annotation.MethodArguments.VisualAlignment * @param FactoryInterface $factory * @param ConfigInterface $config * @param array &$sharedInstances @@ -64,7 +67,7 @@ public function create($type, array $arguments = []) */ public function get($type) { - $type = ltrim($type, '\\'); + $type = \ltrim($type, '\\'); $type = $this->_config->getPreference($type); if (!isset($this->_sharedInstances[$type])) { $this->_sharedInstances[$type] = $this->_factory->create($type); @@ -74,6 +77,7 @@ public function get($type) /** * Configure di instance + * * Note: All arguments should be pre-processed (sort order, translations, etc) before passing to method configure. * * @param array $configuration @@ -83,4 +87,15 @@ public function configure(array $configuration) { $this->_config->extend($configuration); } + + /** + * Disable show ObjectManager internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php b/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php index 90e014d5ee17d..3667c517ec0fd 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php +++ b/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php @@ -8,9 +8,6 @@ use Magento\Framework\ObjectManager\Profiler\Tree\Item; -/** - * Class Log - */ class Log { /** @@ -44,11 +41,11 @@ class Log protected $stats = ['total' => 0, 'used' => 0, 'unused' => 0]; /** - * Constructor + * Destructor */ - public function __construct() + public function __destruct() { - register_shutdown_function([$this, 'display']); + $this->display(); } /** diff --git a/lib/internal/Magento/Framework/ObjectManager/README.md b/lib/internal/Magento/Framework/ObjectManager/README.md index 5fccda9eaad75..1cecd7fc4576c 100644 --- a/lib/internal/Magento/Framework/ObjectManager/README.md +++ b/lib/internal/Magento/Framework/ObjectManager/README.md @@ -1,3 +1,3 @@ # ObjectManager -**ObjectManager** library is responsible for constructing objects and injecting dependencies based on module di.xml files. \ No newline at end of file +**ObjectManager** library is responsible for constructing objects and injecting dependencies based on module di.xml files. diff --git a/lib/internal/Magento/Framework/ObjectManager/RegisterShutdownInterface.php b/lib/internal/Magento/Framework/ObjectManager/RegisterShutdownInterface.php new file mode 100644 index 0000000000000..b8e36308dcc45 --- /dev/null +++ b/lib/internal/Magento/Framework/ObjectManager/RegisterShutdownInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\ObjectManager; + +interface RegisterShutdownInterface +{ + /** + * Register shutdown for all implementations of this type + * + * @return void + */ + public function registerShutdown(); +} diff --git a/lib/internal/Magento/Framework/ObjectManager/ResetAfterRequestInterface.php b/lib/internal/Magento/Framework/ObjectManager/ResetAfterRequestInterface.php new file mode 100644 index 0000000000000..3ed92d8b30044 --- /dev/null +++ b/lib/internal/Magento/Framework/ObjectManager/ResetAfterRequestInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\ObjectManager; + +/** + * This interface is used to reset service's mutable state, and similar problems, after request has been sent in + * Stateful application server and can be used in other long running processes where mutable state in services can + * cause issues. + */ +interface ResetAfterRequestInterface +{ + /** + * Resets mutable state and/or resources in objects that need to be cleaned after a response has been sent. + * + * @return void + */ + public function _resetState(): void; +} diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleMixedProxy.txt b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleMixedProxy.txt index fca3300f2ed8d..43ba469df54bd 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleMixedProxy.txt +++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleMixedProxy.txt @@ -68,7 +68,17 @@ class SampleMixed_Proxy extends SampleMixed implements \Magento\Framework\Object */ public function __clone() { - $this->_subject = clone $this->_getSubject(); + if ($this->_subject) { + $this->_subject = clone $this->_getSubject(); + } + } + + /** + * Debug proxied instance + */ + public function __debugInfo() + { + return ['i' => $this->_subject]; } /** diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt index 6ee7bdcbaf3a3..05929184df051 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt +++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt @@ -68,7 +68,17 @@ class Sample_Proxy extends Sample implements \Magento\Framework\ObjectManager\No */ public function __clone() { - $this->_subject = clone $this->_getSubject(); + if ($this->_subject) { + $this->_subject = clone $this->_getSubject(); + } + } + + /** + * Debug proxied instance + */ + public function __debugInfo() + { + return ['i' => $this->_subject]; } /** diff --git a/lib/internal/Magento/Framework/Option/README.md b/lib/internal/Magento/Framework/Option/README.md index 689cc2023dede..cdae5652a90a9 100644 --- a/lib/internal/Magento/Framework/Option/README.md +++ b/lib/internal/Magento/Framework/Option/README.md @@ -1 +1 @@ -This module is used to create option values in models to value-label pairs that are used in forms. The model must implement Magento\Framework\Option\ArrayInterface or an exception will be thrown. \ No newline at end of file +This module is used to create option values in models to value-label pairs that are used in forms. The model must implement Magento\Framework\Option\ArrayInterface or an exception will be thrown. diff --git a/lib/internal/Magento/Framework/Phrase/README.md b/lib/internal/Magento/Framework/Phrase/README.md index 779979edbb489..5fb87063cb18f 100644 --- a/lib/internal/Magento/Framework/Phrase/README.md +++ b/lib/internal/Magento/Framework/Phrase/README.md @@ -5,4 +5,4 @@ Class *\Magento\Framework\Phrase* calls renderer to make the translation of the * Placeholder render - it replaces placeholders with parameters for substitution. It is the default render if none is set for the Phrase. * Translate render - it is a base renderer that implements text translations. * Inline render - it adds inline translate part to text translation and returns the strings by a template. - * Composite render - it can have several renderers, calls each renderer for processing the text. Array of renderer class names pass into composite render constructor as a parameter. \ No newline at end of file + * Composite render - it can have several renderers, calls each renderer for processing the text. Array of renderer class names pass into composite render constructor as a parameter. diff --git a/lib/internal/Magento/Framework/Pricing/Price/Collection.php b/lib/internal/Magento/Framework/Pricing/Price/Collection.php index 51ef59bf416fc..7e034c7fe71cd 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/Collection.php +++ b/lib/internal/Magento/Framework/Pricing/Price/Collection.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Pricing\Price; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\SaleableInterface; /** @@ -14,7 +15,7 @@ * @api * @since 100.0.2 */ -class Collection implements \Iterator +class Collection implements \Iterator, ResetAfterRequestInterface { /** * @var Pool @@ -74,6 +75,14 @@ public function __construct( $this->priceModels = []; } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->priceModels = []; + } + /** * Reset the Collection to the first element * diff --git a/lib/internal/Magento/Framework/Profiler/Driver/Standard.php b/lib/internal/Magento/Framework/Profiler/Driver/Standard.php index 6278a4b6d1537..03a6fbf51fc78 100644 --- a/lib/internal/Magento/Framework/Profiler/Driver/Standard.php +++ b/lib/internal/Magento/Framework/Profiler/Driver/Standard.php @@ -1,7 +1,5 @@ <?php /** - * Standard profiler driver that uses outputs for displaying profiling results. - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -12,6 +10,9 @@ use Magento\Framework\Profiler\Driver\Standard\Stat; use Magento\Framework\Profiler\DriverInterface; +/** + * Standard profiler driver that uses outputs for displaying profiling results. + */ class Standard implements DriverInterface { /** @@ -37,7 +38,14 @@ public function __construct(array $config = null) { $this->_initOutputs($config); $this->_initStat($config); - register_shutdown_function([$this, 'display']); + } + + /** + * Destructor + */ + public function __destruct() + { + $this->display(); } /** @@ -125,7 +133,7 @@ protected function _getOutputFactory(array $config = null) /** * Init timers statistics object from configuration or create new one * - * @param array $config|null + * @param array|null $config * @return void */ protected function _initStat(array $config = null) diff --git a/lib/internal/Magento/Framework/Profiler/README.md b/lib/internal/Magento/Framework/Profiler/README.md index 07441d3607be6..acace9c459fa6 100644 --- a/lib/internal/Magento/Framework/Profiler/README.md +++ b/lib/internal/Magento/Framework/Profiler/README.md @@ -1,8 +1,9 @@ A library for profiling source code. This is a manual type of profiler, when programmer adds profiling instructions explicitly inline to the source code. Features: + * Measures time between tags (events), number of calls and calculates average time * Measures memory usage * Allows nesting of events and enforces its integrity, and measures aggregated stats of nested elements * Allows configuring filters for tags - * Provides various output formats out of the box: direct HTML output and CSV-file \ No newline at end of file + * Provides various output formats out of the box: direct HTML output and CSV-file diff --git a/lib/internal/Magento/Framework/RegexValidator.php b/lib/internal/Magento/Framework/RegexValidator.php new file mode 100644 index 0000000000000..77274f0f52247 --- /dev/null +++ b/lib/internal/Magento/Framework/RegexValidator.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validator\RegexFactory; + +class RegexValidator extends RegexFactory +{ + + /** + * @var RegexFactory + */ + private RegexFactory $regexValidatorFactory; + + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9,.]+[a-z0-9_,.]*$/i'; + + /** + * @param RegexFactory|null $regexValidatorFactory + */ + public function __construct( + ?RegexFactory $regexValidatorFactory = null + ) { + $this->regexValidatorFactory = $regexValidatorFactory + ?: ObjectManager::getInstance()->get(RegexFactory::class); + } + + /** + * Validates parameter regex + * + * @param string $params + * @param string $pattern + * @return bool + */ + public function validateParamRegex(string $params, string $pattern = self::VALIDATION_RULE_PATTERN): bool + { + $validator = $this->regexValidatorFactory->create(['pattern' => $pattern]); + + if ($params && !$validator->isValid($params)) { + return false; + } + + return true; + } +} diff --git a/lib/internal/Magento/Framework/Registry.php b/lib/internal/Magento/Framework/Registry.php index b5944729fd1a1..8211bd460d540 100644 --- a/lib/internal/Magento/Framework/Registry.php +++ b/lib/internal/Magento/Framework/Registry.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Registry model. Used to manage values in registry * @@ -13,9 +15,10 @@ * * @api * @deprecated 102.0.0 + * @see Nothing * @since 100.0.2 */ -class Registry +class Registry implements ResetAfterRequestInterface { /** * Registry collection @@ -29,8 +32,8 @@ class Registry * * @param string $key * @return mixed - * * @deprecated 102.0.0 + * @see Nothing */ public function registry($key) { @@ -48,8 +51,8 @@ public function registry($key) * @param bool $graceful * @return void * @throws \RuntimeException - * * @deprecated 102.0.0 + * @see Nothing */ public function register($key, $value, $graceful = false) { @@ -67,18 +70,12 @@ public function register($key, $value, $graceful = false) * * @param string $key * @return void - * * @deprecated 102.0.0 + * @see Nothing */ public function unregister($key) { if (isset($this->_registry[$key])) { - if (is_object($this->_registry[$key]) - && method_exists($this->_registry[$key], '__destruct') - && is_callable([$this->_registry[$key], '__destruct']) - ) { - $this->_registry[$key]->__destruct(); - } unset($this->_registry[$key]); } } @@ -91,4 +88,12 @@ public function __destruct() $keys = array_keys($this->_registry); array_walk($keys, [$this, 'unregister']); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_registry = []; + } } diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index b3e1a7f2e309b..5c21e27ec06a6 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -7,6 +7,7 @@ namespace Magento\Framework\Search\Request; use Magento\Framework\Api\SortOrder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; use Magento\Framework\Search\RequestInterface; @@ -18,7 +19,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Builder +class Builder implements ResetAfterRequestInterface { /** * @var ObjectManagerInterface @@ -259,4 +260,12 @@ private function buildDimensions(array $dimensionsData) } return $dimensions; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/lib/internal/Magento/Framework/Serialize/README.md b/lib/internal/Magento/Framework/Serialize/README.md index d900f89208a54..2c4f9894d707c 100644 --- a/lib/internal/Magento/Framework/Serialize/README.md +++ b/lib/internal/Magento/Framework/Serialize/README.md @@ -5,5 +5,5 @@ * *Json* - default implementation. Uses PHP native json_encode/json_decode functions; * *JsonHexTag* - default implementation. Uses PHP native json_encode/json_decode functions with `JSON_HEX_TAG` option enabled; * *Serialize* - less secure than *Json*, but gives higher performance on big arrays. Uses PHP native serialize/unserialize functions, does not unserialize objects on PHP 7. - -Using *Serialize* implementation directly is discouraged, always use *SerializerInterface*, using *Serialize* implementation may lead to security vulnerabilities. \ No newline at end of file + +Using *Serialize* implementation directly is discouraged, always use *SerializerInterface*, using *Serialize* implementation may lead to security vulnerabilities. diff --git a/lib/internal/Magento/Framework/Session/README.md b/lib/internal/Magento/Framework/Session/README.md index 3a5f3b32bcf74..909620e1b449e 100644 --- a/lib/internal/Magento/Framework/Session/README.md +++ b/lib/internal/Magento/Framework/Session/README.md @@ -1,4 +1,4 @@ -#Session +# Session * **SessionManager** Manages active sessions and session metadata. * **SaveHandler** Supports filesystem and database storage of session data. diff --git a/lib/internal/Magento/Framework/Session/RequestAwareSessionManager.php b/lib/internal/Magento/Framework/Session/RequestAwareSessionManager.php new file mode 100644 index 0000000000000..728932370b1bb --- /dev/null +++ b/lib/internal/Magento/Framework/Session/RequestAwareSessionManager.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Session; + +use Magento\Framework\ObjectManager\RegisterShutdownInterface; + +/** + * Session Manager instance used to register shutdown script for Application Server + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RequestAwareSessionManager extends Generic implements RegisterShutdownInterface +{ + /** + * @inheritDoc + */ + public function registerShutDown() + { + $this->writeClose(); + } +} diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index 67b02465a9dfc..f79f0900c2c28 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\Session; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Session\Config\ConfigInterface; /** @@ -13,7 +14,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ -class SessionManager implements SessionManagerInterface +class SessionManager implements SessionManagerInterface, ResetAfterRequestInterface { /** * Default options when a call destroy() @@ -193,7 +194,7 @@ public function start() $this->validator->validate($this); $this->renewCookie(null); - register_shutdown_function([$this, 'writeClose']); + $this->registerShutdown(); $this->_addHost(); \Magento\Framework\Profiler::stop('session_start'); @@ -206,6 +207,16 @@ public function start() return $this; } + /** + * Execute after script terminates + * + * @return void + */ + public function registerShutdown() + { + register_shutdown_function([$this, 'writeClose']); + } + /** * Renew session cookie to prolong session * @@ -621,4 +632,19 @@ private function initIniOptions() } } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + session_id(''); + } + session_name('PHPSESSID'); + session_unset(); + static::$urlHostCache = []; + $_SESSION = []; + } } diff --git a/lib/internal/Magento/Framework/Session/Storage.php b/lib/internal/Magento/Framework/Session/Storage.php index 6fedcb6390acf..d2fd1ac536a37 100644 --- a/lib/internal/Magento/Framework/Session/Storage.php +++ b/lib/internal/Magento/Framework/Session/Storage.php @@ -1,13 +1,16 @@ <?php /** - * Default session storage - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Session; -class Storage extends \Magento\Framework\DataObject implements StorageInterface +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + +/** + * Default session storage + */ +class Storage extends \Magento\Framework\DataObject implements StorageInterface, ResetAfterRequestInterface { /** * Namespace of storage @@ -16,6 +19,14 @@ class Storage extends \Magento\Framework\DataObject implements StorageInterface */ protected $namespace; + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_data = []; + } + /** * Constructor * @@ -29,7 +40,7 @@ public function __construct($namespace = 'default', array $data = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function init(array $data) { @@ -38,10 +49,11 @@ public function init(array $data) $this->setData($data[$namespace]); } $_SESSION[$namespace] = & $this->_data; + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public function getNamespace() { diff --git a/lib/internal/Magento/Framework/Setup/README.md b/lib/internal/Magento/Framework/Setup/README.md index 860ce4bc7ca50..286756642c5d8 100644 --- a/lib/internal/Magento/Framework/Setup/README.md +++ b/lib/internal/Magento/Framework/Setup/README.md @@ -1,5 +1,5 @@ **Setup** provides interfaces that should be used or implemented by Setup data and schema installs, upgrades and uninstalls. - + Implement `InstallSchemaInterface` and/or `UpgradeSchemaInterface` for DB schema install and/or upgrade. Implement `InstallDataInterface` and/or `UpgradeDataInterface` for DB data install and/or upgrade. Implement `UninstallInterface` for handling data removal during module uninstall. diff --git a/lib/internal/Magento/Framework/Simplexml/Config.php b/lib/internal/Magento/Framework/Simplexml/Config.php index 9f403e3451dcc..8b0d5413bc765 100644 --- a/lib/internal/Magento/Framework/Simplexml/Config.php +++ b/lib/internal/Magento/Framework/Simplexml/Config.php @@ -11,6 +11,7 @@ * @api * @since 100.0.2 */ +#[\AllowDynamicProperties] class Config { /** @@ -31,6 +32,7 @@ class Config * Xpath describing nodes in configuration that need to be extended * * @example <allResources extends="/config/modules//resource"/> + * @var string */ protected $_xpathExtends = "//*[@extends]"; diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php index ee88f461b3e57..1268a4ab70a39 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php @@ -376,7 +376,7 @@ private function appendTimeIfNeeded(string $date, bool $includeTime, string $tim $timezone ); $timestamp = $formatter->parse($date); - if (!$timestamp) { + if ($timestamp === false) { throw new LocalizedException( new Phrase( 'Could not append time to DateTime' diff --git a/lib/internal/Magento/Framework/Stdlib/README.md b/lib/internal/Magento/Framework/Stdlib/README.md index d6f877953ab87..3937bfc9a4592 100644 --- a/lib/internal/Magento/Framework/Stdlib/README.md +++ b/lib/internal/Magento/Framework/Stdlib/README.md @@ -1,2 +1,2 @@ The Stdlib library contains utility classes that extend or relate to base PHP classes. Modules should use the CookieManager to get and set cookies instead of the built-in cookie functions for improved security. -Other classes provide convenient methods for dealing with arrays, boolean, date/time, and strings. \ No newline at end of file +Other classes provide convenient methods for dealing with arrays, boolean, date/time, and strings. diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index 87e10981c802c..7ccffe89a4318 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -40,41 +40,43 @@ */ class PhpCookieManagerTest extends TestCase { - const COOKIE_NAME = 'cookie_name'; - const SENSITIVE_COOKIE_NAME_NO_METADATA_HTTPS = 'sensitive_cookie_name_no_metadata_https'; - const SENSITIVE_COOKIE_NAME_NO_METADATA_NOT_HTTPS = 'sensitive_cookie_name_no_metadata_not_https'; - const SENSITIVE_COOKIE_NAME_NO_DOMAIN_NO_PATH = 'sensitive_cookie_name_no_domain_no_path'; - const SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH = 'sensitive_cookie_name_with_domain_and_path'; - const PUBLIC_COOKIE_NAME_NO_METADATA = 'public_cookie_name_no_metadata'; - const PUBLIC_COOKIE_NAME_DEFAULT_VALUES = 'public_cookie_name_default_values'; - const PUBLIC_COOKIE_NAME_SOME_FIELDS_SET = 'public_cookie_name_some_fields_set'; - const MAX_COOKIE_SIZE_TEST_NAME = 'max_cookie_size_test_name'; - const MAX_NUM_COOKIE_TEST_NAME = 'max_num_cookie_test_name'; - const DELETE_COOKIE_NAME = 'delete_cookie_name'; - const DELETE_COOKIE_NAME_NO_METADATA = 'delete_cookie_name_no_metadata'; - const EXCEPTION_COOKIE_NAME = 'exception_cookie_name'; - const COOKIE_VALUE = 'cookie_value'; - const DEFAULT_VAL = 'default'; - const COOKIE_SECURE = true; - const COOKIE_NOT_SECURE = false; - const COOKIE_HTTP_ONLY = true; - const COOKIE_NOT_HTTP_ONLY = false; - const COOKIE_EXPIRE_END_OF_SESSION = 0; + public const COOKIE_NAME = 'cookie_name'; + public const SENSITIVE_COOKIE_NAME_NO_METADATA_HTTPS = 'sensitive_cookie_name_no_metadata_https'; + public const SENSITIVE_COOKIE_NAME_NO_METADATA_NOT_HTTPS = 'sensitive_cookie_name_no_metadata_not_https'; + public const SENSITIVE_COOKIE_NAME_NO_DOMAIN_NO_PATH = 'sensitive_cookie_name_no_domain_no_path'; + public const SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH = 'sensitive_cookie_name_with_domain_and_path'; + public const PUBLIC_COOKIE_NAME_NO_METADATA = 'public_cookie_name_no_metadata'; + public const PUBLIC_COOKIE_NAME_DEFAULT_VALUES = 'public_cookie_name_default_values'; + public const PUBLIC_COOKIE_NAME_SOME_FIELDS_SET = 'public_cookie_name_some_fields_set'; + public const MAX_COOKIE_SIZE_TEST_NAME = 'max_cookie_size_test_name'; + public const MAX_NUM_COOKIE_TEST_NAME = 'max_num_cookie_test_name'; + public const DELETE_COOKIE_NAME = 'delete_cookie_name'; + public const DELETE_COOKIE_NAME_NO_METADATA = 'delete_cookie_name_no_metadata'; + public const EXCEPTION_COOKIE_NAME = 'exception_cookie_name'; + public const COOKIE_VALUE = 'cookie_value'; + public const DEFAULT_VAL = 'default'; + public const COOKIE_SECURE = true; + public const COOKIE_NOT_SECURE = false; + public const COOKIE_HTTP_ONLY = true; + public const COOKIE_NOT_HTTP_ONLY = false; + public const COOKIE_EXPIRE_END_OF_SESSION = 0; /** * Mapping from constant names to functions that handle the assertions. + * + * @var string[] */ protected static $functionTestAssertionMapping = [ - self::DELETE_COOKIE_NAME => 'self::assertDeleteCookie', - self::DELETE_COOKIE_NAME_NO_METADATA => 'self::assertDeleteCookieWithNoMetadata', - self::SENSITIVE_COOKIE_NAME_NO_METADATA_HTTPS => 'self::assertSensitiveCookieWithNoMetaDataHttps', - self::SENSITIVE_COOKIE_NAME_NO_METADATA_NOT_HTTPS => 'self::assertSensitiveCookieWithNoMetaDataNotHttps', - self::SENSITIVE_COOKIE_NAME_NO_DOMAIN_NO_PATH => 'self::assertSensitiveCookieNoDomainNoPath', - self::SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH => 'self::assertSensitiveCookieWithDomainAndPath', - self::PUBLIC_COOKIE_NAME_NO_METADATA => 'self::assertPublicCookieWithNoMetaData', - self::PUBLIC_COOKIE_NAME_DEFAULT_VALUES => 'self::assertPublicCookieWithDefaultValues', - self::PUBLIC_COOKIE_NAME_SOME_FIELDS_SET => 'self::assertPublicCookieWithSomeFieldSet', - self::MAX_COOKIE_SIZE_TEST_NAME => 'self::assertCookieSize', + self::DELETE_COOKIE_NAME => self::class . '::assertDeleteCookie', + self::DELETE_COOKIE_NAME_NO_METADATA => self::class . '::assertDeleteCookieWithNoMetadata', + self::SENSITIVE_COOKIE_NAME_NO_METADATA_HTTPS => self::class . '::assertSensitiveCookieWithNoMetaDataHttps', + self::SENSITIVE_COOKIE_NAME_NO_METADATA_NOT_HTTPS => self::class . '::assertSensitiveCookieWithNoMetaDataNotHttps', //phpcs:ignore + self::SENSITIVE_COOKIE_NAME_NO_DOMAIN_NO_PATH => self::class . '::assertSensitiveCookieNoDomainNoPath', + self::SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH => self::class . '::assertSensitiveCookieWithDomainAndPath', //phpcs:ignore + self::PUBLIC_COOKIE_NAME_NO_METADATA => self::class . '::assertPublicCookieWithNoMetaData', + self::PUBLIC_COOKIE_NAME_DEFAULT_VALUES => self::class . '::assertPublicCookieWithDefaultValues', + self::PUBLIC_COOKIE_NAME_SOME_FIELDS_SET => self::class . '::assertPublicCookieWithSomeFieldSet', + self::MAX_COOKIE_SIZE_TEST_NAME => self::class . '::assertCookieSize', ]; /** @@ -83,8 +85,6 @@ class PhpCookieManagerTest extends TestCase protected $objectManager; /** - * Cookie Manager - * * @var PhpCookieManager */ protected $cookieManager; diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Adapter/SqlVersionProviderTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Adapter/SqlVersionProviderTest.php index 854f5523968d9..33d94aa8e282f 100644 --- a/lib/internal/Magento/Framework/Test/Unit/DB/Adapter/SqlVersionProviderTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Adapter/SqlVersionProviderTest.php @@ -107,7 +107,7 @@ public function executeDataProvider(): array { return [ 'MariaDB-10.6' => [ - ['version' => '10.6.10-MariaDB'], + ['version' => '10.6.12-MariaDB'], '10.6.' ], 'MariaDB-10.4' => [ diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index c6cd5e446b79e..4e7e96f9a6253 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -328,6 +328,11 @@ public function escapeHtmlDataProvider() 'expected' => ' some text', 'allowedTags' => ['span'], ], + 'text with japanese lang' => [ + 'data' => '<span>だ だ だ some text in tags<br /></span>', + 'expected' => '<span>だ だ だ some text in tags</span>', + 'allowedTags' => ['span'], + ], ]; } diff --git a/lib/internal/Magento/Framework/Test/Unit/File/Pdf/ImageResource/ImageFactoryTest.php b/lib/internal/Magento/Framework/Test/Unit/File/Pdf/ImageResource/ImageFactoryTest.php new file mode 100644 index 0000000000000..ad1f582ecc964 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/File/Pdf/ImageResource/ImageFactoryTest.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Test\Unit\File\Pdf\ImageResource; + +use Exception; +use Magento\Framework\File\Pdf\ImageResource\ImageFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use PHPUnit\Framework\TestCase; +use Zend_Pdf_Resource_Image_Jpeg; +use Zend_Pdf_Resource_Image_Png; + +/** + * Class for testing Magento\Framework\File\Pdf\ImageResource\ImageFactory + * @SuppressWarnings(PHPMD.LongVariable) + */ +class ImageFactoryTest extends TestCase +{ + /** + * Url of AWS main image + */ + private const REMOTE_IMAGE_PATH = 'https://a0.awsstatic.com/libra-css/' . + 'images/logos/aws_smile-header-desktop-en-white_59x35.png'; + + /** + * @var \Magento\Framework\File\Pdf\ImageResource\ImageFactory + */ + private ImageFactory $factory; + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Zend_Pdf_Exception + */ + public function testFactoryWithLocalImage(): void + { + $filesystemMock = $this->createMock(Filesystem::class); + + $tempFileResouceFromBucketOrDisk = tmpfile(); + $tempFilenameFromBucketOrDisk = stream_get_meta_data($tempFileResouceFromBucketOrDisk)['uri']; + $readerMock = $this->createMock(Read::class); + $readerMock->method('isFile') + ->with($tempFilenameFromBucketOrDisk) + ->willReturn(true); + $imagePath = $this->generateImageByConfig( + [ + 'image-width' => 36, + 'image-height' => 69, + 'image-name' => $tempFilenameFromBucketOrDisk + ] + ); + + $readerMock->method('readFile') + ->with($tempFilenameFromBucketOrDisk) + ->willReturn(file_get_contents($tempFilenameFromBucketOrDisk)); + + $filesystemMock->method('getDirectoryRead') + ->willReturn($readerMock); + + $this->factory = new ImageFactory($filesystemMock); + + /** @var \Zend_Pdf_Resource_Image_Jpeg|\Zend_Pdf_Resource_Image_Png|\Zend_Pdf_Resource_Image_Tiff $result */ + $result = $this->factory->factory($tempFilenameFromBucketOrDisk); + unlink($imagePath); + $this->assertEquals(69, $result->getPixelHeight()); + $this->assertEquals(36, $result->getPixelWidth()); + $this->assertInstanceOf(Zend_Pdf_Resource_Image_Jpeg::class, $result); + } + + /** + * @param array<mixed> $config + * @return array|string|string[]|null + * @throws \Exception + */ + private function generateImageByConfig(array $config) + { + // phpcs:disable Magento2.Functions.DiscouragedFunction + $binaryData = ''; + $data = str_split(sha1($config['image-name']), 2); + foreach ($data as $item) { + $binaryData .= base_convert($item, 16, 2); + } + $binaryData = str_split($binaryData, 1); + + $image = imagecreate($config['image-width'], $config['image-height']); + $bgColor = imagecolorallocate($image, 240, 240, 240); + // mt_rand() here is not for cryptographic use. + // phpcs:ignore Magento2.Security.InsecureFunction + $fgColor = imagecolorallocate($image, mt_rand(0, 230), mt_rand(0, 230), mt_rand(0, 230)); + $colors = [$fgColor, $bgColor]; + imagefilledrectangle($image, 0, 0, $config['image-width'], $config['image-height'], $bgColor); + + for ($row = 10; $row < ($config['image-height'] - 10); $row += 10) { + for ($col = 10; $col < ($config['image-width'] - 10); $col += 10) { + if (next($binaryData) === false) { + reset($binaryData); + } + imagefilledrectangle($image, $row, $col, $row + 10, $col + 10, $colors[current($binaryData)]); + } + } + + $imagePath = $config['image-name']; + $imagePath = preg_replace('|/{2,}|', '/', $imagePath); + $memory = fopen('php://memory', 'r+'); + if (!imagejpeg($image, $memory)) { + throw new Exception('Could not create picture ' . $imagePath); + } + file_put_contents($imagePath, stream_get_contents($memory, -1, 0)); + fclose($memory); + imagedestroy($image); + // phpcs:enable + + return $imagePath; + } + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Zend_Pdf_Exception + */ + public function testFactoryWithRemoteImage(): void + { + $filesystemMock = $this->createMock(Filesystem::class); + + $readerMock = $this->createMock(Read::class); + $readerMock->method('isFile') + ->with(self::REMOTE_IMAGE_PATH) + ->willReturn(true); + $readerMock->method('readFile') + ->with(self::REMOTE_IMAGE_PATH) + ->willReturn(file_get_contents(self::REMOTE_IMAGE_PATH)); + + $filesystemMock->method('getDirectoryRead') + ->willReturn($readerMock); + + $this->factory = new ImageFactory($filesystemMock); + + /** @var \Zend_Pdf_Resource_Image_Jpeg|\Zend_Pdf_Resource_Image_Png|\Zend_Pdf_Resource_Image_Tiff $result */ + $result = $this->factory->factory(self::REMOTE_IMAGE_PATH); + $this->assertEquals(35, $result->getPixelHeight()); + $this->assertEquals(59, $result->getPixelWidth()); + $this->assertInstanceOf(Zend_Pdf_Resource_Image_Png::class, $result); + } +} diff --git a/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager.php b/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager.php index 5f9dc48557861..4588ecef0d849 100644 --- a/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager.php +++ b/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestListenerDefaultImplementation; +use Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager\TestProvidesServiceInterface; /** * The event listener which instantiates ObjectManager before test run @@ -36,6 +37,12 @@ public function startTest(Test $test): void $objectManagerMock = $test->getMockBuilder(ObjectManagerInterface::class) ->getMockForAbstractClass(); $createMockCallback = function ($type) use ($test) { + if ($test instanceof TestProvidesServiceInterface) { + $serviceObject = $test->getServiceForObjectManager($type); + if ($serviceObject) { + return $serviceObject; + } + } return $test->getMockBuilder($type) ->disableOriginalConstructor() ->getMockForAbstractClass(); diff --git a/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager/TestProvidesServiceInterface.php b/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager/TestProvidesServiceInterface.php new file mode 100644 index 0000000000000..f721a6f4ceb26 --- /dev/null +++ b/lib/internal/Magento/Framework/TestFramework/Unit/Listener/ReplaceObjectManager/TestProvidesServiceInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager; + +interface TestProvidesServiceInterface +{ + /** + * Gets a service object from a test to use by the mock object manager + * + * @param string $type + * @return object|null + */ + public function getServiceForObjectManager(string $type) : ?object; +} diff --git a/lib/internal/Magento/Framework/Translate.php b/lib/internal/Magento/Framework/Translate.php index ea0911eb7000b..7dc7622c2eed2 100644 --- a/lib/internal/Magento/Framework/Translate.php +++ b/lib/internal/Magento/Framework/Translate.php @@ -11,6 +11,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Translate library @@ -18,17 +19,15 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class Translate implements \Magento\Framework\TranslateInterface +class Translate implements \Magento\Framework\TranslateInterface, ResetAfterRequestInterface { - const CONFIG_AREA_KEY = 'area'; - const CONFIG_LOCALE_KEY = 'locale'; - const CONFIG_SCOPE_KEY = 'scope'; - const CONFIG_THEME_KEY = 'theme'; - const CONFIG_MODULE_KEY = 'module'; + public const CONFIG_AREA_KEY = 'area'; + public const CONFIG_LOCALE_KEY = 'locale'; + public const CONFIG_SCOPE_KEY = 'scope'; + public const CONFIG_THEME_KEY = 'theme'; + public const CONFIG_MODULE_KEY = 'module'; /** - * Locale code - * * @var string */ protected $_localeCode; @@ -592,6 +591,7 @@ protected function _saveCache() * * @return \Magento\Framework\Serialize\SerializerInterface * @deprecated 101.0.0 + * @see we don't recommend this approach anymore */ private function getSerializer() { @@ -601,4 +601,14 @@ private function getSerializer() } return $this->serializer; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_config = []; + $this->_data = []; + $this->_localeCode = null; + } } diff --git a/lib/internal/Magento/Framework/Translate/README.md b/lib/internal/Magento/Framework/Translate/README.md index 7bb68ad88aca3..b4b1ff43dfc3c 100644 --- a/lib/internal/Magento/Framework/Translate/README.md +++ b/lib/internal/Magento/Framework/Translate/README.md @@ -11,4 +11,4 @@ Magento provides an *Inline Translation* tool that allows inline editing of phra * State - It can disable, enable, suspend and resume inline translation. * *StateInterface* and a *State* class * Resource - It stores and retrieve translation array - * *ResourceInterface* \ No newline at end of file + * *ResourceInterface* diff --git a/lib/internal/Magento/Framework/Translate/Test/Unit/AdapterTest.php b/lib/internal/Magento/Framework/Translate/Test/Unit/AdapterTest.php index 4ca549ec1a170..007fa19b2563f 100644 --- a/lib/internal/Magento/Framework/Translate/Test/Unit/AdapterTest.php +++ b/lib/internal/Magento/Framework/Translate/Test/Unit/AdapterTest.php @@ -61,6 +61,6 @@ public function testTranslateNoProxy() */ public function testUnderscoresTranslation() { - $this->markTestIncomplete('MAGETWO-1012: i18n Improvements - Localization/Translations'); + $this->markTestSkipped('MAGETWO-1012: i18n Improvements - Localization/Translations'); } } diff --git a/lib/internal/Magento/Framework/Unserialize/README.md b/lib/internal/Magento/Framework/Unserialize/README.md index 2dbf7436aa60f..c473cdf4e865c 100644 --- a/lib/internal/Magento/Framework/Unserialize/README.md +++ b/lib/internal/Magento/Framework/Unserialize/README.md @@ -1 +1 @@ -This library is deprecated, please use Magento\Framework\Serialize\SerializerInterface instead. \ No newline at end of file +This library is deprecated, please use Magento\Framework\Serialize\SerializerInterface instead. diff --git a/lib/internal/Magento/Framework/Url.php b/lib/internal/Magento/Framework/Url.php index 0ecdaf2209a2c..d84eae7a9b3b1 100644 --- a/lib/internal/Magento/Framework/Url.php +++ b/lib/internal/Magento/Framework/Url.php @@ -7,9 +7,11 @@ namespace Magento\Framework; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Url\HostChecker; +// phpcs:disable Magento2.Annotation /** * URL * @@ -64,7 +66,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ -class Url extends \Magento\Framework\DataObject implements \Magento\Framework\UrlInterface +class Url extends \Magento\Framework\DataObject implements \Magento\Framework\UrlInterface, ResetAfterRequestInterface { /** * Configuration data cache @@ -1008,6 +1010,7 @@ public function getRebuiltUrl($url) * @param string $value * @return string * @deprecated 101.0.0 + * @see \Magento\Framework\Escaper::escapeUrl */ public function escape($value) { @@ -1159,6 +1162,7 @@ protected function getRouteParamsResolver() * * @return \Magento\Framework\Url\ModifierInterface * @deprecated 101.0.0 + * @see \Magento\Framework\Url\ModifierInterface */ private function getUrlModifier() { @@ -1176,6 +1180,7 @@ private function getUrlModifier() * * @return Escaper * @deprecated 101.0.0 + * @see \Magento\Framework\Escaper */ private function getEscaper() { @@ -1185,4 +1190,14 @@ private function getEscaper() } return $this->escaper; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_data = []; + $this->cacheUrl = []; + self::$_configDataCache = []; + } } diff --git a/lib/internal/Magento/Framework/Url/QueryParamsResolver.php b/lib/internal/Magento/Framework/Url/QueryParamsResolver.php index 06353a038bd39..1e38df4941511 100644 --- a/lib/internal/Magento/Framework/Url/QueryParamsResolver.php +++ b/lib/internal/Magento/Framework/Url/QueryParamsResolver.php @@ -8,7 +8,7 @@ class QueryParamsResolver extends \Magento\Framework\DataObject implements QueryParamsResolverInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function getQuery($escape = false) { @@ -25,7 +25,7 @@ public function getQuery($escape = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuery($data) { @@ -37,7 +37,7 @@ public function setQuery($data) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQueryParam($key, $data) { @@ -52,7 +52,7 @@ public function setQueryParam($key, $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function getQueryParams() { @@ -70,7 +70,7 @@ public function getQueryParams() } /** - * {@inheritdoc} + * @inheritdoc */ public function setQueryParams(array $data) { @@ -78,7 +78,7 @@ public function setQueryParams(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function addQueryParams(array $data) { @@ -99,4 +99,12 @@ public function addQueryParams(array $data) return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_data = []; + } } diff --git a/lib/internal/Magento/Framework/Validator.php b/lib/internal/Magento/Framework/Validator.php index 731eba1a7c26b..21885aec5bea4 100644 --- a/lib/internal/Magento/Framework/Validator.php +++ b/lib/internal/Magento/Framework/Validator.php @@ -7,6 +7,7 @@ namespace Magento\Framework; use Laminas\Validator\Translator\TranslatorInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Validator\AbstractValidator; /** @@ -15,7 +16,7 @@ * @api * @since 100.0.2 */ -class Validator extends AbstractValidator +class Validator extends AbstractValidator implements ResetAfterRequestInterface { /** * Validator chain @@ -84,4 +85,12 @@ public function setTranslator(?TranslatorInterface $translator = null) } return parent::setTranslator($translator); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_validators = []; + } } diff --git a/lib/internal/Magento/Framework/Validator/AbstractValidator.php b/lib/internal/Magento/Framework/Validator/AbstractValidator.php index c90092f64d95c..7b5c895685a3f 100644 --- a/lib/internal/Magento/Framework/Validator/AbstractValidator.php +++ b/lib/internal/Magento/Framework/Validator/AbstractValidator.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Validator; use Laminas\Validator\Translator\TranslatorInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Abstract validator class. @@ -14,7 +15,7 @@ * @api * @since 100.0.2 */ -abstract class AbstractValidator implements ValidatorInterface +abstract class AbstractValidator implements ValidatorInterface, ResetAfterRequestInterface { /** * @var TranslatorInterface|null @@ -31,6 +32,15 @@ abstract class AbstractValidator implements ValidatorInterface */ protected $_messages = []; + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_translator = null; + $this->_messages = []; + } + /** * Set default translator instance * diff --git a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php index 315c3ee204864..a6722dc91c629 100644 --- a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php +++ b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php @@ -7,28 +7,31 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem; use Magento\Framework\Math\Random; use Magento\Framework\View\Asset; +use Magento\Framework\View\Asset\MergeStrategyInterface; +use Magento\Framework\View\Url\CssResolver; /** * The actual merging service */ -class Direct implements \Magento\Framework\View\Asset\MergeStrategyInterface +class Direct implements MergeStrategyInterface { - /**#@+ + /** * Delimiters for merging files of various content type */ - const MERGE_DELIMITER_JS = ';'; - - const MERGE_DELIMITER_EMPTY = ''; + private const MERGE_DELIMITER_JS = ';'; - /**#@-*/ + private const MERGE_DELIMITER_EMPTY = ''; - /**#@-*/ + /** + * @var Filesystem + */ private $filesystem; /** - * @var \Magento\Framework\View\Url\CssResolver + * @var CssResolver */ private $cssUrlResolver; @@ -38,13 +41,13 @@ class Direct implements \Magento\Framework\View\Asset\MergeStrategyInterface private $mathRandom; /** - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Framework\View\Url\CssResolver $cssUrlResolver + * @param Filesystem $filesystem + * @param CssResolver $cssUrlResolver * @param Random|null $mathRandom */ public function __construct( - \Magento\Framework\Filesystem $filesystem, - \Magento\Framework\View\Url\CssResolver $cssUrlResolver, + Filesystem $filesystem, + CssResolver $cssUrlResolver, Random $mathRandom = null ) { $this->filesystem = $filesystem; @@ -61,9 +64,8 @@ public function merge(array $assetsToMerge, Asset\LocalInterface $resultAsset) $filePath = $resultAsset->getPath(); $tmpFilePath = $filePath . $this->mathRandom->getUniqueHash('_'); $staticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); - $tmpDir = $this->filesystem->getDirectoryWrite(DirectoryList::TMP); - $tmpDir->writeFile($tmpFilePath, $mergedContent); - $tmpDir->renameFile($tmpFilePath, $filePath, $staticDir); + $staticDir->writeFile($tmpFilePath, $mergedContent); + $staticDir->renameFile($tmpFilePath, $filePath, $staticDir); } /** diff --git a/lib/internal/Magento/Framework/View/Asset/Minification.php b/lib/internal/Magento/Framework/View/Asset/Minification.php index b8628fe6b6162..3d2bd9ee61d4b 100644 --- a/lib/internal/Magento/Framework/View/Asset/Minification.php +++ b/lib/internal/Magento/Framework/View/Asset/Minification.php @@ -7,13 +7,14 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\State; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Helper class for static files minification related processes. * @api * @since 100.0.2 */ -class Minification +class Minification implements ResetAfterRequestInterface { /** * XML path for asset minification configuration @@ -228,4 +229,12 @@ private function getAreaFromPath(string $filename): string } return $area; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->configCache = []; + } } diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 0cd9030c269e7..245ee66c2d5c1 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -6,11 +6,12 @@ namespace Magento\Framework\View\Asset; -use Magento\Framework\UrlInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\View\Design\Theme\ThemeProviderInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; /** * A repository service for view assets @@ -18,8 +19,9 @@ * * @api * @since 100.0.2 + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting */ -class Repository +class Repository implements ResetAfterRequestInterface { /** * Scope separator for module notation of file ID @@ -467,4 +469,13 @@ private function getRepositoryFilesMap($fileId, array $params) $repositoryMap = ObjectManager::getInstance()->get(RepositoryMap::class); return $repositoryMap->getMap($fileId, $params); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->fallbackContext = []; + $this->fileContext = []; + } } diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index 7c02dc0675206..e40f6f64e463b 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -7,7 +7,10 @@ namespace Magento\Framework\View\Element; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\DataObject\IdentityInterface; /** @@ -51,6 +54,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl /** * @var \Magento\Framework\Session\SidResolverInterface * @deprecated 102.0.5 Not used anymore. + * @see Session Id's In URL */ protected $_sidResolver; @@ -166,6 +170,11 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl */ protected $_cache; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @var LockGuardedCacheLoader */ @@ -199,6 +208,7 @@ public function __construct( $this->_localeDate = $context->getLocaleDate(); $this->inlineTranslation = $context->getInlineTranslation(); $this->lockQuery = $context->getLockGuardedCacheLoader(); + if (isset($data['jsLayout'])) { $this->jsLayout = $data['jsLayout']; unset($data['jsLayout']); @@ -880,6 +890,7 @@ public static function extractModuleName($className) * @param array|null $allowedTags * @return string * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. + * @see Escaper Usage */ public function escapeHtml($data, $allowedTags = null) { @@ -893,6 +904,7 @@ public function escapeHtml($data, $allowedTags = null) * @return string * @since 101.0.0 * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. + * @see Escaper Usage */ public function escapeJs($string) { @@ -907,6 +919,7 @@ public function escapeJs($string) * @return string * @since 101.0.0 * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. + * @see Escaper Usage */ public function escapeHtmlAttr($string, $escapeSingleQuote = true) { @@ -920,6 +933,7 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true) * @return string * @since 101.0.0 * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. + * @see Escaper Usage */ public function escapeCss($string) { @@ -947,6 +961,7 @@ public function stripTags($data, $allowableTags = null, $allowHtmlEntities = fal * @param string $string * @return string * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. + * @see Escaper Usage */ public function escapeUrl($string) { @@ -959,6 +974,7 @@ public function escapeUrl($string) * @param string $data * @return string * @deprecated 101.0.0 + * @see Escaper Usage */ public function escapeXssInUrl($data) { @@ -974,6 +990,7 @@ public function escapeXssInUrl($data) * @param bool $addSlashes * @return string * @deprecated 101.0.0 + * @see Escaper Usage */ public function escapeQuote($data, $addSlashes = false) { @@ -988,6 +1005,7 @@ public function escapeQuote($data, $addSlashes = false) * * @return string|array * @deprecated 101.0.0 + * @see Escaper Usage */ public function escapeJsQuote($data, $quote = '\'') { @@ -1035,8 +1053,12 @@ public function getCacheKey() $key = array_values($key); // ignore array keys + $key[] = (string)$this->getDeploymentConfig()->get( + ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY + ); + $key = implode('|', $key); - $key = sha1($key); // use hashing to hide potentially private data + $key = hash('sha256', $key); // use hashing to hide potentially private data return static::CACHE_KEY_PREFIX . $key; } @@ -1175,10 +1197,25 @@ public function getVar($name, $module = null) * * @return bool * @deprecated + * @see https://developer.adobe.com/commerce/php/development/cache/page/private-content * @since 103.0.1 */ public function isScopePrivate() { return $this->_isScopePrivate; } + + /** + * Get DeploymentConfig + * + * @return DeploymentConfig + */ + private function getDeploymentConfig() : DeploymentConfig + { + if ($this->deploymentConfig === null) { + $this->deploymentConfig = ObjectManager::getInstance() + ->get(DeploymentConfig::class); + } + return $this->deploymentConfig; + } } diff --git a/lib/internal/Magento/Framework/View/README.md b/lib/internal/Magento/Framework/View/README.md index 6c04c6ab59861..ffe5be98414fa 100644 --- a/lib/internal/Magento/Framework/View/README.md +++ b/lib/internal/Magento/Framework/View/README.md @@ -1 +1 @@ -View library contains common infrastructure to work with view related components. \ No newline at end of file +View library contains common infrastructure to work with view related components. diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php index 6c3737e197736..85663ab5d277a 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php @@ -24,108 +24,123 @@ */ class DirectTest extends TestCase { - /** - * @var Random|MockObject - */ - protected $mathRandomMock; /** * @var Direct */ - protected $object; + private $model; /** - * @var MockObject|CssResolver + * @var Random|MockObject */ - protected $cssUrlResolver; + private $mathRandomMock; /** - * @var MockObject|WriteInterface + * @var MockObject|CssResolver */ - protected $staticDir; + private $cssUrlResolverMock; /** * @var MockObject|WriteInterface */ - protected $tmpDir; + private $staticDirMock; /** * @var MockObject|LocalInterface */ - protected $resultAsset; + private $resultAssetMock; + /** + * @inheridoc + */ protected function setUp(): void { - $this->cssUrlResolver = $this->createMock(CssResolver::class); - $filesystem = $this->createMock(Filesystem::class); - $this->staticDir = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $this->tmpDir = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $filesystem->expects($this->any()) + $this->cssUrlResolverMock = $this->createMock(CssResolver::class); + $this->staticDirMock = $this->getMockForAbstractClass(WriteInterface::class); + $tmpDir = $this->getMockForAbstractClass(WriteInterface::class); + + $filesystemMock = $this->createMock(Filesystem::class); + $filesystemMock->expects($this->any()) ->method('getDirectoryWrite') ->willReturnMap([ - [DirectoryList::STATIC_VIEW, DriverPool::FILE, $this->staticDir], - [DirectoryList::TMP, DriverPool::FILE, $this->tmpDir], + [DirectoryList::STATIC_VIEW, DriverPool::FILE, $this->staticDirMock], + [DirectoryList::TMP, DriverPool::FILE, $tmpDir], ]); - $this->resultAsset = $this->createMock(File::class); - $this->mathRandomMock = $this->getMockBuilder(Random::class) - ->disableOriginalConstructor() - ->getMock(); - $this->object = new Direct($filesystem, $this->cssUrlResolver, $this->mathRandomMock); + + $this->resultAssetMock = $this->createMock(File::class); + $this->mathRandomMock = $this->createMock(Random::class); + $this->model = new Direct($filesystemMock, $this->cssUrlResolverMock, $this->mathRandomMock); } public function testMergeNoAssets() { $uniqId = '_b3bf82fa6e140594420fa90982a8e877'; - $this->resultAsset->expects($this->once())->method('getPath')->willReturn('foo/result'); - $this->staticDir->expects($this->never())->method('writeFile'); + $this->resultAssetMock->expects($this->once()) + ->method('getPath') + ->willReturn('foo/result'); $this->mathRandomMock->expects($this->once()) ->method('getUniqueHash') ->willReturn($uniqId); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, ''); - $this->tmpDir->expects($this->once())->method('renameFile') - ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); - $this->object->merge([], $this->resultAsset); + $this->staticDirMock->expects($this->once()) + ->method('writeFile') + ->with('foo/result' . $uniqId, ''); + $this->staticDirMock->expects($this->once()) + ->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDirMock); + + $this->model->merge([], $this->resultAssetMock); } public function testMergeGeneric() { $uniqId = '_be50ccf992fd81818c1a2645d1a29e92'; - $this->resultAsset->expects($this->once())->method('getPath')->willReturn('foo/result'); $assets = $this->prepareAssetsToMerge([' one', 'two']); // note leading space intentionally - $this->staticDir->expects($this->never())->method('writeFile'); + + $this->resultAssetMock->expects($this->once()) + ->method('getPath') + ->willReturn('foo/result'); + $this->mathRandomMock->expects($this->once()) ->method('getUniqueHash') ->willReturn($uniqId); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, 'onetwo'); - $this->tmpDir->expects($this->once())->method('renameFile') - ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); - $this->object->merge($assets, $this->resultAsset); + + $this->staticDirMock->expects($this->once()) + ->method('writeFile') + ->with('foo/result' . $uniqId, 'onetwo'); + $this->staticDirMock->expects($this->once()) + ->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDirMock); + + $this->model->merge($assets, $this->resultAssetMock); } public function testMergeCss() { $uniqId = '_f929c374767e00712449660ea673f2f5'; - $this->resultAsset->expects($this->exactly(3)) + $this->resultAssetMock->expects($this->exactly(3)) ->method('getPath') ->willReturn('foo/result'); - $this->resultAsset->expects($this->any())->method('getContentType')->willReturn('css'); + $this->resultAssetMock->expects($this->atLeastOnce()) + ->method('getContentType') + ->willReturn('css'); $assets = $this->prepareAssetsToMerge(['one', 'two']); - $this->cssUrlResolver->expects($this->exactly(2)) + $this->cssUrlResolverMock->expects($this->exactly(2)) ->method('relocateRelativeUrls') ->will($this->onConsecutiveCalls('1', '2')); - $this->cssUrlResolver->expects($this->once()) + $this->cssUrlResolverMock->expects($this->once()) ->method('aggregateImportDirectives') ->with('12') ->willReturn('1020'); $this->mathRandomMock->expects($this->once()) ->method('getUniqueHash') ->willReturn($uniqId); - $this->staticDir->expects($this->never())->method('writeFile'); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, '1020'); - $this->tmpDir->expects($this->once())->method('renameFile') - ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); - $this->object->merge($assets, $this->resultAsset); + $this->staticDirMock->expects($this->once()) + ->method('writeFile') + ->with('foo/result' . $uniqId, '1020'); + $this->staticDirMock->expects($this->once()) + ->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDirMock); + + $this->model->merge($assets, $this->resultAssetMock); } /** @@ -134,7 +149,7 @@ public function testMergeCss() * @param array $data * @return array */ - private function prepareAssetsToMerge(array $data) + private function prepareAssetsToMerge(array $data): array { $result = []; foreach ($data as $content) { diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php index fa425b413560b..55c84f9b5ca3a 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php @@ -9,10 +9,13 @@ use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Config\View; use Magento\Framework\Escaper; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -69,11 +72,26 @@ class AbstractBlockTest extends TestCase */ private $lockQuery; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + /** * @return void */ protected function setUp(): void { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMockForAbstractClass(); + \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); $this->eventManagerMock = $this->getMockForAbstractClass(EventManagerInterface::class); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->cacheStateMock = $this->getMockForAbstractClass(CacheStateInterface::class); @@ -108,13 +126,18 @@ protected function setUp(): void $contextMock->expects($this->once()) ->method('getLockGuardedCacheLoader') ->willReturn($this->lockQuery); + $this->block = $this->getMockForAbstractClass( AbstractBlock::class, [ 'context' => $contextMock, - 'data' => [], + 'data' => [] ] ); + $this->deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); } /** @@ -224,9 +247,20 @@ public function testGetCacheKey() */ public function testGetCacheKeyByName() { + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(DeploymentConfig::class) + ->willReturn($this->deploymentConfig); + + $this->deploymentConfig->expects($this->any()) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY) + ->willReturn('448198e08af35844a42d3c93c1ef4e03'); + $nameInLayout = 'testBlock'; $this->block->setNameInLayout($nameInLayout); - $cacheKey = sha1($nameInLayout); + $encryptionKey = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $cacheKey = hash('sha256', $nameInLayout . '|' . $encryptionKey); $this->assertEquals(AbstractBlock::CACHE_KEY_PREFIX . $cacheKey, $this->block->getCacheKey()); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Helper/SecureHtmlRenderer/HtmlRendererTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Helper/SecureHtmlRenderer/HtmlRendererTest.php index 44b6f66c4380a..4a708cec34243 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Helper/SecureHtmlRenderer/HtmlRendererTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Helper/SecureHtmlRenderer/HtmlRendererTest.php @@ -11,9 +11,13 @@ use Magento\Framework\View\Helper\SecureHtmlRender\HtmlRenderer; use Magento\Framework\View\Helper\SecureHtmlRender\TagData; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; class HtmlRendererTest extends TestCase { + /** @var Escaper|MockObject */ + protected $escaperMock; + /** * @return void */ diff --git a/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php index 3737d86d2b1f6..6419295753051 100644 --- a/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php @@ -28,24 +28,24 @@ */ class ErrorProcessor { - const DEFAULT_SHUTDOWN_FUNCTION = 'apiShutdownFunction'; + public const DEFAULT_SHUTDOWN_FUNCTION = 'apiShutdownFunction'; - const DEFAULT_ERROR_HTTP_CODE = 500; + public const DEFAULT_ERROR_HTTP_CODE = 500; - const DEFAULT_RESPONSE_CHARSET = 'UTF-8'; + public const DEFAULT_RESPONSE_CHARSET = 'UTF-8'; - const INTERNAL_SERVER_ERROR_MSG = 'Internal Error. Details are available in Magento log file. Report ID: %s'; + public const INTERNAL_SERVER_ERROR_MSG = 'Internal Error. Details are available in Magento log file. Report ID: %s'; - /**#@+ + /** * Error data representation formats. */ - const DATA_FORMAT_JSON = 'json'; - - const DATA_FORMAT_XML = 'xml'; + public const DATA_FORMAT_JSON = 'json'; - /**#@-*/ + public const DATA_FORMAT_XML = 'xml'; - /**#@-*/ + /** + * @var \Magento\Framework\Json\Encoder $encoder + */ protected $encoder; /** diff --git a/lib/internal/Magento/Framework/Webapi/Request.php b/lib/internal/Magento/Framework/Webapi/Request.php index 3628bfbafb060..21bf440736f4e 100644 --- a/lib/internal/Magento/Framework/Webapi/Request.php +++ b/lib/internal/Magento/Framework/Webapi/Request.php @@ -1,7 +1,5 @@ <?php /** - * Web API request. - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -15,17 +13,22 @@ use Magento\Framework\Phrase; use Magento\Framework\Stdlib\StringUtils; +/** + * Web API request. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class Request extends HttpRequest implements RequestInterface { /** * Name of query parameter to specify services for which to generate schema */ - const REQUEST_PARAM_SERVICES = 'services'; + public const REQUEST_PARAM_SERVICES = 'services'; /** * services parameter value to indicate that a schema for all services should be generated */ - const ALL_SERVICES = 'all'; + public const ALL_SERVICES = 'all'; /** * Modify pathInfo: strip down the front name and query parameters. @@ -54,24 +57,6 @@ public function __construct( $this->setPathInfo($pathInfo); } - /** - * {@inheritdoc} - * - * Added CGI environment support. - */ - public function getHeader($header, $default = false) - { - $headerValue = parent::getHeader($header, $default); - if ($headerValue == false) { - /** Workaround for hhvm environment */ - $header = 'REDIRECT_HTTP_' . strtoupper(str_replace('-', '_', $header)); - if (isset($_SERVER[$header])) { - $headerValue = $_SERVER[$header]; - } - } - return $headerValue; - } - /** * Identify versions of resources that should be used for API configuration generation. * diff --git a/lib/internal/Magento/Framework/Webapi/RequestAwareErrorProcessor.php b/lib/internal/Magento/Framework/Webapi/RequestAwareErrorProcessor.php new file mode 100644 index 0000000000000..46857566b45cf --- /dev/null +++ b/lib/internal/Magento/Framework/Webapi/RequestAwareErrorProcessor.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Webapi; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Request\Http as Request; +use Magento\Framework\HTTP\PhpEnvironment\Response; +use Magento\Framework\ObjectManager\RegisterShutdownInterface; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Request dependent Error Processor + */ +class RequestAwareErrorProcessor extends ErrorProcessor implements RegisterShutdownInterface +{ + /** + * @var Request + */ + private Request $request; + + /** + * @var Response + */ + private Response $response; + + /** + * @param \Magento\Framework\Json\Encoder $encoder + * @param \Magento\Framework\App\State $appState + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Filesystem $filesystem + * @param Json|null $serializer + * @param Request|null $request + * @param Response|null $response + */ + public function __construct( + \Magento\Framework\Json\Encoder $encoder, + \Magento\Framework\App\State $appState, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Filesystem $filesystem, + Json $serializer = null, + Request $request = null, + Response $response = null + ) { + $this->request = $request ?: ObjectManager::getInstance()->get(Request::class); + $this->response = $response ?: ObjectManager::getInstance()->get(Response::class); + parent::__construct( + $encoder, + $appState, + $logger, + $filesystem, + $serializer + ); + } + + /** + * @inheritDoc + */ + public function renderErrorMessage( + $errorMessage, + $trace = 'Trace is not available.', + $httpCode = self::DEFAULT_ERROR_HTTP_CODE + ) { + if (isset($this->request->getServer()['HTTP_ACCEPT']) && + strstr($this->request->getServer()['HTTP_ACCEPT'], self::DATA_FORMAT_XML)) { + $output = $this->_formatError($errorMessage, $trace, $httpCode, self::DATA_FORMAT_XML); + $mimeType = 'application/xml'; + } else { + // Default format is JSON + $output = $this->_formatError($errorMessage, $trace, $httpCode, self::DATA_FORMAT_JSON); + $mimeType = 'application/json'; + } + if (!headers_sent()) { + $this->response->setStatusCode($httpCode ? $httpCode : self::DEFAULT_ERROR_HTTP_CODE); + $this->response->getHeaders()->addHeaderLine( + 'Content-Type: ' . $mimeType . '; charset=' . self::DEFAULT_RESPONSE_CHARSET + ); + } + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput + echo $output; + } + + /** + * @inheritDoc + */ + public function registerShutdown() + { + $this->apiShutdownFunction(); + } +} diff --git a/lib/internal/Magento/Framework/Webapi/Response.php b/lib/internal/Magento/Framework/Webapi/Response.php index 88ef420005a23..83a8f0c8cc792 100644 --- a/lib/internal/Magento/Framework/Webapi/Response.php +++ b/lib/internal/Magento/Framework/Webapi/Response.php @@ -1,39 +1,44 @@ <?php /** - * Web API response. - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Webapi; +use Magento\Framework\App\Response\HttpInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + +/** + * Web Api response + */ class Response extends \Magento\Framework\HTTP\PhpEnvironment\Response implements - \Magento\Framework\App\Response\HttpInterface + HttpInterface, + ResetAfterRequestInterface { /** * Character set which must be used in response. */ - const RESPONSE_CHARSET = 'utf-8'; + public const RESPONSE_CHARSET = 'utf-8'; /**#@+ * Default message types. */ - const MESSAGE_TYPE_SUCCESS = 'success'; + public const MESSAGE_TYPE_SUCCESS = 'success'; - const MESSAGE_TYPE_ERROR = 'error'; + public const MESSAGE_TYPE_ERROR = 'error'; - const MESSAGE_TYPE_WARNING = 'warning'; + public const MESSAGE_TYPE_WARNING = 'warning'; /**#@- */ /**#@+ * Success HTTP response codes. */ - const HTTP_OK = 200; + public const HTTP_OK = 200; - /**#@-*/ - - /**#@-*/ + /** + * @var array + */ protected $_messages = []; /** @@ -94,4 +99,13 @@ public function clearMessages() $this->_messages = []; return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->clearMessages(); + parent::_resetState(); + } } diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Request.php b/lib/internal/Magento/Framework/Webapi/Rest/Request.php index fc39f594bbcd4..64a0335b22b73 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Request.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Request.php @@ -157,7 +157,6 @@ public function getBodyParams() public function getContentType() { $headerValue = $this->getHeader('Content-Type'); - if (!$headerValue) { throw new \Magento\Framework\Exception\InputException(new Phrase('Content-Type header is empty.')); } diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererFactory.php b/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererFactory.php index 9c7d71f5bc86a..dc78d3fdc0eb8 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererFactory.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererFactory.php @@ -34,7 +34,7 @@ class RendererFactory */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, - \Magento\Framework\Webapi\Rest\Request $request, + \Magento\Framework\App\RequestInterface $request, array $renders = [] ) { $this->_objectManager = $objectManager; @@ -68,16 +68,13 @@ public function get() */ protected function _getRendererClass() { - $acceptTypes = $this->_request->getAcceptTypes(); - if (!is_array($acceptTypes)) { - $acceptTypes = [$acceptTypes]; - } + $acceptTypes = $this->getAcceptTypes(); foreach ($acceptTypes as $acceptType) { foreach ($this->_renders as $rendererConfig) { $rendererType = $rendererConfig['type']; - if ($acceptType == $rendererType || $acceptType == current( - explode('/', $rendererType ?? '') - ) . '/*' || $acceptType == '*/*' + if ($acceptType == $rendererType + || $acceptType == current(explode('/', $rendererType ?? '')) . '/*' + || $acceptType == '*/*' ) { return $rendererConfig['model']; } @@ -94,4 +91,42 @@ protected function _getRendererClass() \Magento\Framework\Webapi\Exception::HTTP_NOT_ACCEPTABLE ); } + + /** + * Retrieve accept types understandable by requester in a form of array sorted by quality in descending order. + * + * @return string[] + */ + private function getAcceptTypes() + { + $qualityToTypes = []; + $orderedTypes = []; + + foreach (preg_split('/,\s*/', $this->_request->getHeader('Accept') ?? '') as $definition) { + $typeWithQ = explode(';', $definition); + $mimeType = trim(array_shift($typeWithQ)); + + // check MIME type validity + if (!preg_match('~^([0-9a-z*+\-]+)(?:/([0-9a-z*+\-\.]+))?$~i', $mimeType)) { + continue; + } + $quality = '1.0'; + // default value for quality + + if ($typeWithQ) { + $qAndValue = explode('=', $typeWithQ[0]); + + if (2 == count($qAndValue)) { + $quality = $qAndValue[1]; + } + } + $qualityToTypes[$quality][$mimeType] = true; + } + krsort($qualityToTypes); + + foreach ($qualityToTypes as $typeList) { + $orderedTypes += $typeList; + } + return empty($orderedTypes) ? ['*/*'] : array_keys($orderedTypes); + } } diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/RendererFactoryTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/RendererFactoryTest.php index 567cad5737c48..f9808b7c13be7 100644 --- a/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/RendererFactoryTest.php +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/RendererFactoryTest.php @@ -1,7 +1,5 @@ <?php /** - * Test Rest renderer factory class. - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -18,6 +16,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test Rest renderer factory class. + */ class RendererFactoryTest extends TestCase { /** @var RendererFactory */ @@ -57,10 +58,8 @@ protected function setUp(): void */ public function testGet() { - $acceptTypes = ['application/json']; - /** Mock request getAcceptTypes method to return specified value. */ - $this->_requestMock->expects($this->once())->method('getAcceptTypes')->willReturn($acceptTypes); + $this->_requestMock->expects($this->once())->method('getHeader')->willReturn('application/json'); /** Mock renderer. */ $rendererMock = $this->getMockBuilder( Json::class @@ -84,14 +83,14 @@ public function testGet() */ public function testGetWithWrongAcceptHttpHeader() { - /** Mock request to return empty Accept Types. */ - $this->_requestMock->expects($this->once())->method('getAcceptTypes')->willReturn(''); + /** Mock request to return invalid Accept Types. */ + $this->_requestMock->expects($this->once())->method('getHeader')->willReturn('invalid'); try { $this->_factory->get(); $this->fail("Exception is expected to be raised"); } catch (Exception $e) { $exceptionMessage = 'Server cannot match any of the given Accept HTTP header media type(s) ' . - 'from the request: "" with media types from the config of response renderer.'; + 'from the request: "invalid" with media types from the config of response renderer.'; $this->assertInstanceOf(Exception::class, $e, 'Exception type is invalid'); $this->assertEquals($exceptionMessage, $e->getMessage(), 'Exception message is invalid'); $this->assertEquals( @@ -107,9 +106,8 @@ public function testGetWithWrongAcceptHttpHeader() */ public function testGetWithWrongRendererClass() { - $acceptTypes = ['application/json']; /** Mock request getAcceptTypes method to return specified value. */ - $this->_requestMock->expects($this->once())->method('getAcceptTypes')->willReturn($acceptTypes); + $this->_requestMock->expects($this->once())->method('getHeader')->willReturn('application/json'); /** Mock object to return \Magento\Framework\DataObject */ $this->_objectManagerMock->expects( $this->once() diff --git a/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php index ca75da4a93eed..2c35a3630904e 100644 --- a/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php +++ b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php @@ -46,7 +46,7 @@ class InputArraySizeLimitValue * @param DeploymentConfig $deploymentConfig */ public function __construct( - Request $request, + \Magento\Framework\App\RequestInterface $request, DeploymentConfig $deploymentConfig ) { $this->request = $request; diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index 7c0f2aa1be76a..488f553682957 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -25,8 +25,8 @@ "lib-libxml": "*", "colinmollenhour/php-redis-session-abstract": "^1.5", "composer/composer": "^2.0, !=2.2.16", - "ezyang/htmlpurifier": "^4.14", - "guzzlehttp/guzzle": "^7.4", + "ezyang/htmlpurifier": "^4.16", + "guzzlehttp/guzzle": "^7.5", "laminas/laminas-code": "^4.5", "laminas/laminas-escaper": "^2.10", "laminas/laminas-file": "^2.11", @@ -50,8 +50,8 @@ "symfony/intl": "^5.4", "symfony/process": "^5.4", "tedivm/jshrink": "^1.4", - "webonyx/graphql-php": "^14.11", - "wikimedia/less.php": "^3.0" + "webonyx/graphql-php": "^15.0", + "wikimedia/less.php": "^3.2" }, "archive": { "exclude": [ diff --git a/lib/web/css/docs/source/README.md b/lib/web/css/docs/source/README.md index 872a01eae4a50..872bf41aad5c4 100644 --- a/lib/web/css/docs/source/README.md +++ b/lib/web/css/docs/source/README.md @@ -34,7 +34,9 @@ The library provides the ability to customize all of the following user interfac * list of theme variables # Magento UI library file structure + Magento UI library is located under `/lib/web/` folder. It and employs: + * `css/` folder where the library files are placed * `fonts/` folder where default and icon fonts are placed * `images/` folder where default images are placed @@ -135,6 +137,7 @@ Magento UI library is located under `/lib/web/` folder. It and employs: └── jquery/ (Library javascript files) ``` +   # Magento UI library naming convention @@ -183,6 +186,7 @@ Private variables: @paddingleft; @__font-size; ``` +   # Less mixins naming @@ -194,19 +198,23 @@ A mixin name can consist of one or several words, concatenated with one hyphen. #### Examples: ##### Acceptable: + ```css .mixin-name() {} .transition() {} .mixin() {} ._button-gradient() {} ``` + ##### Unacceptable: + ```css .mixinName() {} .__transition() {} .MiXiN() {} ._button--gradient() {} ``` +   # Less Code Standards @@ -236,6 +244,7 @@ Please verified that you use spaces instead tabs: Add space before opening brace and line break after. And line break before closing brace. ##### Not recommended: + ```css .nav{color: @nav__color;} ``` @@ -575,6 +584,7 @@ Using class names this way contributes to acceptable levels of understandability Write selector name together in single line, don't use concatenation ##### Not recommended: + ```css .product { ... @@ -605,6 +615,7 @@ Generic names are simply a fallback for elements that have no particular or no m Using functional or generic names reduces the probability of unnecessary document or template changes. ##### Not recommended: + ```css .foo-1901 { ... @@ -757,7 +768,7 @@ Using shorthand properties is useful for code efficiency and understandability. padding-top: 0; ``` - ##### Recommended: +##### Recommended: ```css border-top: 0; @@ -831,6 +842,7 @@ Omit leading "0"s in values, use dot instead If variables are local and used only in module scope it should be located in module file, on the top of the file with general comment Example **_module.less**: + ```css ... diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 077a81b096685..17736feef448f 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -1140,7 +1140,7 @@ fotoramaVersion = '4.6.4'; function addEvent(el, e, fn, bool) { if (!e) return; - el.addEventListener ? el.addEventListener(e, fn, {passive: true}) : el.attachEvent('on' + e, fn); + el.addEventListener ? el.addEventListener(e, fn, {passive: !!bool}) : el.attachEvent('on' + e, fn); } /** @@ -1519,7 +1519,7 @@ fotoramaVersion = '4.6.4'; addEvent(el, 'touchmove', onMove); addEvent(el, 'touchend', onEnd); - addEvent(document, 'touchstart', onOtherStart); + addEvent(document, 'touchstart', onOtherStart, true); addEvent(document, 'touchend', onOtherEnd); addEvent(document, 'touchcancel', onOtherEnd); diff --git a/lib/web/jquery/fileUploader/README.md b/lib/web/jquery/fileUploader/README.md index 5a13ef4252e44..b48a4b1d5d702 100644 --- a/lib/web/jquery/fileUploader/README.md +++ b/lib/web/jquery/fileUploader/README.md @@ -204,10 +204,13 @@ To run the tests, follow these steps: 1. Start [Docker](https://docs.docker.com/). 2. Install development dependencies: + ```sh npm install ``` + 3. Run the tests: + ```sh npm test ``` diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md index 5759a126aa172..77b6b1195e3de 100644 --- a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md @@ -565,7 +565,7 @@ available. Exif orientation values to correctly display the letter F: -``` +```diagram 1 2 ██████ ██████ ██ ██ diff --git a/lib/web/jquery/jquery-ui.js b/lib/web/jquery/jquery-ui.js index 4eb1385ca1ec8..1a613bf2f94b3 100644 --- a/lib/web/jquery/jquery-ui.js +++ b/lib/web/jquery/jquery-ui.js @@ -1,6 +1,6 @@ -/*! jQuery UI - v1.13.1 - 2022-02-22 +/*! jQuery UI - v1.13.2 - 2022-07-14 * http://jqueryui.com -* Includes: widget.js, position.js, data.js, disable-selection.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/selectmenu.js, widgets/slider.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js * Copyright jQuery Foundation and other contributors; Licensed MIT */ ( function( factory ) { @@ -20,11 +20,11 @@ $.ui = $.ui || {}; -var version = $.ui.version = "1.13.1"; +var version = $.ui.version = "1.13.2"; /*! - * jQuery UI Widget 1.13.1 + * jQuery UI Widget 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -766,7 +766,7 @@ var widget = $.widget; /*! - * jQuery UI Position 1.13.1 + * jQuery UI Position 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1263,7 +1263,7 @@ var position = $.ui.position; /*! - * jQuery UI :data 1.13.1 + * jQuery UI :data 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1292,7 +1292,7 @@ var data = $.extend( $.expr.pseudos, { } ); /*! - * jQuery UI Disable Selection 1.13.1 + * jQuery UI Disable Selection 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1326,709 +1326,728 @@ var disableSelection = $.fn.extend( { } ); + +// Create a local jQuery because jQuery Color relies on it and the +// global may not exist with AMD and a custom build (#10199). +// This module is a noop if used as a regular AMD module. +// eslint-disable-next-line no-unused-vars +var jQuery = $; + + /*! - * jQuery UI Focusable 1.13.1 - * http://jqueryui.com + * jQuery Color Animations v2.2.0 + * https://github.com/jquery/jquery-color * - * Copyright jQuery Foundation and other contributors + * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * http://jquery.org/license + * + * Date: Sun May 10 09:02:36 2020 +0200 */ -//>>label: :focusable Selector -//>>group: Core -//>>description: Selects elements which can be focused. -//>>docs: http://api.jqueryui.com/focusable-selector/ -// Selectors -$.ui.focusable = function( element, hasTabindex ) { - var map, mapName, img, focusableIfVisible, fieldset, - nodeName = element.nodeName.toLowerCase(); + var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " + + "borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap='#" + mapName + "']" ); - return img.length > 0 && img.is( ":visible" ); - } + class2type = {}, + toString = class2type.toString, - if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) { - focusableIfVisible = !element.disabled; + // plusequals test for += 100 -= 100 + rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, - if ( focusableIfVisible ) { + // a set of RE's that can match strings and generate color tuples. + stringParsers = [ { + re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ], + execResult[ 3 ], + execResult[ 4 ] + ]; + } + }, { + re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ] * 2.55, + execResult[ 2 ] * 2.55, + execResult[ 3 ] * 2.55, + execResult[ 4 ] + ]; + } + }, { - // Form controls within a disabled fieldset are disabled. - // However, controls within the fieldset's legend do not get disabled. - // Since controls generally aren't placed inside legends, we skip - // this portion of the check. - fieldset = $( element ).closest( "fieldset" )[ 0 ]; - if ( fieldset ) { - focusableIfVisible = !fieldset.disabled; + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ], 16 ), + execResult[ 4 ] ? + ( parseInt( execResult[ 4 ], 16 ) / 255 ).toFixed( 2 ) : + 1 + ]; } - } - } else if ( "a" === nodeName ) { - focusableIfVisible = element.href || hasTabindex; - } else { - focusableIfVisible = hasTabindex; - } + }, { - return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); -}; + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ), + execResult[ 4 ] ? + ( parseInt( execResult[ 4 ] + execResult[ 4 ], 16 ) / 255 ) + .toFixed( 2 ) : + 1 + ]; + } + }, { + re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + space: "hsla", + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ] / 100, + execResult[ 3 ] / 100, + execResult[ 4 ] + ]; + } + } ], -// Support: IE 8 only -// IE 8 doesn't resolve inherit to visible/hidden for computed values -function visible( element ) { - var visibility = element.css( "visibility" ); - while ( visibility === "inherit" ) { - element = element.parent(); - visibility = element.css( "visibility" ); - } - return visibility === "visible"; -} + // jQuery.Color( ) + color = jQuery.Color = function( color, green, blue, alpha ) { + return new jQuery.Color.fn.parse( color, green, blue, alpha ); + }, + spaces = { + rgba: { + props: { + red: { + idx: 0, + type: "byte" + }, + green: { + idx: 1, + type: "byte" + }, + blue: { + idx: 2, + type: "byte" + } + } + }, -$.extend( $.expr.pseudos, { - focusable: function( element ) { - return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); - } -} ); + hsla: { + props: { + hue: { + idx: 0, + type: "degrees" + }, + saturation: { + idx: 1, + type: "percent" + }, + lightness: { + idx: 2, + type: "percent" + } + } + } + }, + propTypes = { + "byte": { + floor: true, + max: 255 + }, + "percent": { + max: 1 + }, + "degrees": { + mod: 360, + floor: true + } + }, + support = color.support = {}, -var focusable = $.ui.focusable; + // element for support tests + supportElem = jQuery( "<p>" )[ 0 ], + // colors = jQuery.Color.names + colors, + // local aliases of functions called often + each = jQuery.each; -// Support: IE8 Only -// IE8 does not support the form attribute and when it is supplied. It overwrites the form prop -// with a string, so we need to find the proper form. -var form = $.fn._form = function() { - return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); -}; +// determine rgba support immediately +supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; +support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; +// define cache name and alpha properties +// for rgba and hsla spaces +each( spaces, function( spaceName, space ) { + space.cache = "_" + spaceName; + space.props.alpha = { + idx: 3, + type: "percent", + def: 1 + }; +} ); -/*! - * jQuery UI Form Reset Mixin 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); -//>>label: Form Reset Mixin -//>>group: Core -//>>description: Refresh input widgets when their form is reset -//>>docs: http://api.jqueryui.com/form-reset-mixin/ +function getType( obj ) { + if ( obj == null ) { + return obj + ""; + } + return typeof obj === "object" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} -var formResetMixin = $.ui.formResetMixin = { - _formResetHandler: function() { - var form = $( this ); +function clamp( value, prop, allowEmpty ) { + var type = propTypes[ prop.type ] || {}; - // Wait for the form reset to actually happen before refreshing - setTimeout( function() { - var instances = form.data( "ui-form-reset-instances" ); - $.each( instances, function() { - this.refresh(); - } ); - } ); - }, + if ( value == null ) { + return ( allowEmpty || !prop.def ) ? null : prop.def; + } - _bindFormResetHandler: function() { - this.form = this.element._form(); - if ( !this.form.length ) { - return; - } + // ~~ is an short way of doing floor for positive numbers + value = type.floor ? ~~value : parseFloat( value ); - var instances = this.form.data( "ui-form-reset-instances" ) || []; - if ( !instances.length ) { - - // We don't use _on() here because we use a single event handler per form - this.form.on( "reset.ui-form-reset", this._formResetHandler ); - } - instances.push( this ); - this.form.data( "ui-form-reset-instances", instances ); - }, - - _unbindFormResetHandler: function() { - if ( !this.form.length ) { - return; - } - - var instances = this.form.data( "ui-form-reset-instances" ); - instances.splice( $.inArray( this, instances ), 1 ); - if ( instances.length ) { - this.form.data( "ui-form-reset-instances", instances ); - } else { - this.form - .removeData( "ui-form-reset-instances" ) - .off( "reset.ui-form-reset" ); - } + // IE will pass in empty strings as value for alpha, + // which will hit this case + if ( isNaN( value ) ) { + return prop.def; } -}; - - -/*! - * jQuery UI Support for jQuery core 1.8.x and newer 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - */ -//>>label: jQuery 1.8+ Support -//>>group: Core -//>>description: Support version 1.8.x and newer of jQuery core + if ( type.mod ) { + // we add mod before modding to make sure that negatives values + // get converted properly: -10 -> 350 + return ( value + type.mod ) % type.mod; + } -// Support: jQuery 1.9.x or older -// $.expr[ ":" ] is deprecated. -if ( !$.expr.pseudos ) { - $.expr.pseudos = $.expr[ ":" ]; + // for now all property types without mod have min and max + return Math.min( type.max, Math.max( 0, value ) ); } -// Support: jQuery 1.11.x or older -// $.unique has been renamed to $.uniqueSort -if ( !$.uniqueSort ) { - $.uniqueSort = $.unique; -} +function stringParse( string ) { + var inst = color(), + rgba = inst._rgba = []; -// Support: jQuery 2.2.x or older. -// This method has been defined in jQuery 3.0.0. -// Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js -if ( !$.escapeSelector ) { + string = string.toLowerCase(); - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + each( stringParsers, function( _i, parser ) { + var parsed, + match = parser.re.exec( string ), + values = match && parser.parse( match ), + spaceName = parser.space || "rgba"; - var fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { + if ( values ) { + parsed = inst[ spaceName ]( values ); - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } + // if this was an rgba parse the assignment might happen twice + // oh well.... + inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; + rgba = inst._rgba = parsed._rgba; - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + // exit each( stringParsers ) here because we matched + return false; } + } ); - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }; - - $.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); - }; -} + // Found a stringParser that handled it + if ( rgba.length ) { -// Support: jQuery 3.4.x or older -// These methods have been defined in jQuery 3.5.0. -if ( !$.fn.even || !$.fn.odd ) { - $.fn.extend( { - even: function() { - return this.filter( function( i ) { - return i % 2 === 0; - } ); - }, - odd: function() { - return this.filter( function( i ) { - return i % 2 === 1; - } ); + // if this came from a parsed string, force "transparent" when alpha is 0 + // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) + if ( rgba.join() === "0,0,0,0" ) { + jQuery.extend( rgba, colors.transparent ); } - } ); -} - -; -/*! - * jQuery UI Keycode 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + return inst; + } -//>>label: Keycode -//>>group: Core -//>>description: Provide keycodes as keynames -//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ + // named colors + return colors[ string ]; +} +color.fn = jQuery.extend( color.prototype, { + parse: function( red, green, blue, alpha ) { + if ( red === undefined ) { + this._rgba = [ null, null, null, null ]; + return this; + } + if ( red.jquery || red.nodeType ) { + red = jQuery( red ).css( green ); + green = undefined; + } -var keycode = $.ui.keyCode = { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 -}; + var inst = this, + type = getType( red ), + rgba = this._rgba = []; + // more than 1 argument specified - assume ( red, green, blue, alpha ) + if ( green !== undefined ) { + red = [ red, green, blue, alpha ]; + type = "array"; + } -/*! - * jQuery UI Labels 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + if ( type === "string" ) { + return this.parse( stringParse( red ) || colors._default ); + } -//>>label: labels -//>>group: Core -//>>description: Find all the labels associated with a given input -//>>docs: http://api.jqueryui.com/labels/ + if ( type === "array" ) { + each( spaces.rgba.props, function( _key, prop ) { + rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); + } ); + return this; + } + if ( type === "object" ) { + if ( red instanceof color ) { + each( spaces, function( _spaceName, space ) { + if ( red[ space.cache ] ) { + inst[ space.cache ] = red[ space.cache ].slice(); + } + } ); + } else { + each( spaces, function( _spaceName, space ) { + var cache = space.cache; + each( space.props, function( key, prop ) { -var labels = $.fn.labels = function() { - var ancestor, selector, id, labels, ancestors; + // if the cache doesn't exist, and we know how to convert + if ( !inst[ cache ] && space.to ) { - if ( !this.length ) { - return this.pushStack( [] ); - } + // if the value was null, we don't need to copy it + // if the key was alpha, we don't need to copy it either + if ( key === "alpha" || red[ key ] == null ) { + return; + } + inst[ cache ] = space.to( inst._rgba ); + } - // Check control.labels first - if ( this[ 0 ].labels && this[ 0 ].labels.length ) { - return this.pushStack( this[ 0 ].labels ); - } + // this is the only case where we allow nulls for ALL properties. + // call clamp with alwaysAllowEmpty + inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); + } ); - // Support: IE <= 11, FF <= 37, Android <= 2.3 only - // Above browsers do not support control.labels. Everything below is to support them - // as well as document fragments. control.labels does not work on document fragments - labels = this.eq( 0 ).parents( "label" ); + // everything defined but alpha? + if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { - // Look for the label based on the id - id = this.attr( "id" ); - if ( id ) { + // use the default of 1 + if ( inst[ cache ][ 3 ] == null ) { + inst[ cache ][ 3 ] = 1; + } - // We don't search against the document in case the element - // is disconnected from the DOM - ancestor = this.eq( 0 ).parents().last(); + if ( space.from ) { + inst._rgba = space.from( inst[ cache ] ); + } + } + } ); + } + return this; + } + }, + is: function( compare ) { + var is = color( compare ), + same = true, + inst = this; - // Get a full set of top level ancestors - ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); - - // Create a selector for the label based on the id - selector = "label[for='" + $.escapeSelector( id ) + "']"; - - labels = labels.add( ancestors.find( selector ).addBack( selector ) ); - - } - - // Return whatever we have found for labels - return this.pushStack( labels ); -}; - - -/*! - * jQuery UI Scroll Parent 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: scrollParent -//>>group: Core -//>>description: Get the closest ancestor element that is scrollable. -//>>docs: http://api.jqueryui.com/scrollParent/ - - -var scrollParent = $.fn.scrollParent = function( includeHidden ) { - var position = this.css( "position" ), - excludeStaticParent = position === "absolute", - overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, - scrollParent = this.parents().filter( function() { - var parent = $( this ); - if ( excludeStaticParent && parent.css( "position" ) === "static" ) { - return false; + each( spaces, function( _, space ) { + var localCache, + isCache = is[ space.cache ]; + if ( isCache ) { + localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; + each( space.props, function( _, prop ) { + if ( isCache[ prop.idx ] != null ) { + same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); + return same; + } + } ); } - return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + - parent.css( "overflow-x" ) ); - } ).eq( 0 ); - - return position === "fixed" || !scrollParent.length ? - $( this[ 0 ].ownerDocument || document ) : - scrollParent; -}; - - -/*! - * jQuery UI Tabbable 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: :tabbable Selector -//>>group: Core -//>>description: Selects elements which can be tabbed to. -//>>docs: http://api.jqueryui.com/tabbable-selector/ - - -var tabbable = $.extend( $.expr.pseudos, { - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - hasTabindex = tabIndex != null; - return ( !hasTabindex || tabIndex >= 0 ) && $.ui.focusable( element, hasTabindex ); - } -} ); - - -/*! - * jQuery UI Unique ID 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: uniqueId -//>>group: Core -//>>description: Functions to generate and remove uniqueId's -//>>docs: http://api.jqueryui.com/uniqueId/ + return same; + } ); + return same; + }, + _space: function() { + var used = [], + inst = this; + each( spaces, function( spaceName, space ) { + if ( inst[ space.cache ] ) { + used.push( spaceName ); + } + } ); + return used.pop(); + }, + transition: function( other, distance ) { + var end = color( other ), + spaceName = end._space(), + space = spaces[ spaceName ], + startColor = this.alpha() === 0 ? color( "transparent" ) : this, + start = startColor[ space.cache ] || space.to( startColor._rgba ), + result = start.slice(); + end = end[ space.cache ]; + each( space.props, function( _key, prop ) { + var index = prop.idx, + startValue = start[ index ], + endValue = end[ index ], + type = propTypes[ prop.type ] || {}; -var uniqueId = $.fn.extend( { - uniqueId: ( function() { - var uuid = 0; + // if null, don't override start value + if ( endValue === null ) { + return; + } - return function() { - return this.each( function() { - if ( !this.id ) { - this.id = "ui-id-" + ( ++uuid ); + // if null - use end + if ( startValue === null ) { + result[ index ] = endValue; + } else { + if ( type.mod ) { + if ( endValue - startValue > type.mod / 2 ) { + startValue += type.mod; + } else if ( startValue - endValue > type.mod / 2 ) { + startValue -= type.mod; + } } - } ); - }; - } )(), - - removeUniqueId: function() { - return this.each( function() { - if ( /^ui-id-\d+$/.test( this.id ) ) { - $( this ).removeAttr( "id" ); + result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); } } ); - } -} ); - - - -// This file is deprecated -var ie = $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + return this[ spaceName ]( result ); + }, + blend: function( opaque ) { -/*! - * jQuery UI Mouse 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // if we are already opaque - return ourself + if ( this._rgba[ 3 ] === 1 ) { + return this; + } -//>>label: Mouse -//>>group: Widgets -//>>description: Abstracts mouse-based interactions to assist in creating certain widgets. -//>>docs: http://api.jqueryui.com/mouse/ + var rgb = this._rgba.slice(), + a = rgb.pop(), + blend = color( opaque )._rgba; + return color( jQuery.map( rgb, function( v, i ) { + return ( 1 - a ) * blend[ i ] + a * v; + } ) ); + }, + toRgbaString: function() { + var prefix = "rgba(", + rgba = jQuery.map( this._rgba, function( v, i ) { + if ( v != null ) { + return v; + } + return i > 2 ? 1 : 0; + } ); -var mouseHandled = false; -$( document ).on( "mouseup", function() { - mouseHandled = false; -} ); + if ( rgba[ 3 ] === 1 ) { + rgba.pop(); + prefix = "rgb("; + } -var widgetsMouse = $.widget( "ui.mouse", { - version: "1.13.1", - options: { - cancel: "input, textarea, button, select, option", - distance: 1, - delay: 0 + return prefix + rgba.join() + ")"; }, - _mouseInit: function() { - var that = this; + toHslaString: function() { + var prefix = "hsla(", + hsla = jQuery.map( this.hsla(), function( v, i ) { + if ( v == null ) { + v = i > 2 ? 1 : 0; + } - this.element - .on( "mousedown." + this.widgetName, function( event ) { - return that._mouseDown( event ); - } ) - .on( "click." + this.widgetName, function( event ) { - if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { - $.removeData( event.target, that.widgetName + ".preventClickEvent" ); - event.stopImmediatePropagation(); - return false; + // catch 1 and 2 + if ( i && i < 3 ) { + v = Math.round( v * 100 ) + "%"; } + return v; } ); - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.off( "." + this.widgetName ); - if ( this._mouseMoveDelegate ) { - this.document - .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + if ( hsla[ 3 ] === 1 ) { + hsla.pop(); + prefix = "hsl("; } + return prefix + hsla.join() + ")"; }, + toHexString: function( includeAlpha ) { + var rgba = this._rgba.slice(), + alpha = rgba.pop(); - _mouseDown: function( event ) { - - // don't let more than one widget handle mouseStart - if ( mouseHandled ) { - return; + if ( includeAlpha ) { + rgba.push( ~~( alpha * 255 ) ); } - this._mouseMoved = false; - - // We may have missed mouseup (out of window) - if ( this._mouseStarted ) { - this._mouseUp( event ); - } + return "#" + jQuery.map( rgba, function( v ) { - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = ( event.which === 1 ), + // default to 0 when nulls exist + v = ( v || 0 ).toString( 16 ); + return v.length === 1 ? "0" + v : v; + } ).join( "" ); + }, + toString: function() { + return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); + } +} ); +color.fn.parse.prototype = color.fn; - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = ( typeof this.options.cancel === "string" && event.target.nodeName ? - $( event.target ).closest( this.options.cancel ).length : false ); - if ( !btnIsLeft || elIsCancel || !this._mouseCapture( event ) ) { - return true; - } +// hsla conversions adapted from: +// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 - this.mouseDelayMet = !this.options.delay; - if ( !this.mouseDelayMet ) { - this._mouseDelayTimer = setTimeout( function() { - that.mouseDelayMet = true; - }, this.options.delay ); - } +function hue2rgb( p, q, h ) { + h = ( h + 1 ) % 1; + if ( h * 6 < 1 ) { + return p + ( q - p ) * h * 6; + } + if ( h * 2 < 1 ) { + return q; + } + if ( h * 3 < 2 ) { + return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6; + } + return p; +} - if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { - this._mouseStarted = ( this._mouseStart( event ) !== false ); - if ( !this._mouseStarted ) { - event.preventDefault(); - return true; - } - } +spaces.hsla.to = function( rgba ) { + if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { + return [ null, null, null, rgba[ 3 ] ]; + } + var r = rgba[ 0 ] / 255, + g = rgba[ 1 ] / 255, + b = rgba[ 2 ] / 255, + a = rgba[ 3 ], + max = Math.max( r, g, b ), + min = Math.min( r, g, b ), + diff = max - min, + add = max + min, + l = add * 0.5, + h, s; - // Click event may never have fired (Gecko & Opera) - if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { - $.removeData( event.target, this.widgetName + ".preventClickEvent" ); - } + if ( min === max ) { + h = 0; + } else if ( r === max ) { + h = ( 60 * ( g - b ) / diff ) + 360; + } else if ( g === max ) { + h = ( 60 * ( b - r ) / diff ) + 120; + } else { + h = ( 60 * ( r - g ) / diff ) + 240; + } - // These delegates are required to keep context - this._mouseMoveDelegate = function( event ) { - return that._mouseMove( event ); - }; - this._mouseUpDelegate = function( event ) { - return that._mouseUp( event ); - }; + // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% + // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) + if ( diff === 0 ) { + s = 0; + } else if ( l <= 0.5 ) { + s = diff / add; + } else { + s = diff / ( 2 - add ); + } + return [ Math.round( h ) % 360, s, l, a == null ? 1 : a ]; +}; - this.document - .on( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .on( "mouseup." + this.widgetName, this._mouseUpDelegate ); +spaces.hsla.from = function( hsla ) { + if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { + return [ null, null, null, hsla[ 3 ] ]; + } + var h = hsla[ 0 ] / 360, + s = hsla[ 1 ], + l = hsla[ 2 ], + a = hsla[ 3 ], + q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, + p = 2 * l - q; - event.preventDefault(); + return [ + Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), + Math.round( hue2rgb( p, q, h ) * 255 ), + Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), + a + ]; +}; - mouseHandled = true; - return true; - }, - _mouseMove: function( event ) { +each( spaces, function( spaceName, space ) { + var props = space.props, + cache = space.cache, + to = space.to, + from = space.from; - // Only check for mouseups outside the document if you've moved inside the document - // at least once. This prevents the firing of mouseup in the case of IE<9, which will - // fire a mousemove event if content is placed under the cursor. See #7778 - // Support: IE <9 - if ( this._mouseMoved ) { + // makes rgba() and hsla() + color.fn[ spaceName ] = function( value ) { - // IE mouseup check - mouseup happened when mouse was out of window - if ( $.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && - !event.button ) { - return this._mouseUp( event ); + // generate a cache for this space if it doesn't exist + if ( to && !this[ cache ] ) { + this[ cache ] = to( this._rgba ); + } + if ( value === undefined ) { + return this[ cache ].slice(); + } - // Iframe mouseup check - mouseup occurred in another document - } else if ( !event.which ) { + var ret, + type = getType( value ), + arr = ( type === "array" || type === "object" ) ? value : arguments, + local = this[ cache ].slice(); - // Support: Safari <=8 - 9 - // Safari sets which to 0 if you press any of the following keys - // during a drag (#14461) - if ( event.originalEvent.altKey || event.originalEvent.ctrlKey || - event.originalEvent.metaKey || event.originalEvent.shiftKey ) { - this.ignoreMissingWhich = true; - } else if ( !this.ignoreMissingWhich ) { - return this._mouseUp( event ); - } + each( props, function( key, prop ) { + var val = arr[ type === "object" ? key : prop.idx ]; + if ( val == null ) { + val = local[ prop.idx ]; } - } + local[ prop.idx ] = clamp( val, prop ); + } ); - if ( event.which || event.button ) { - this._mouseMoved = true; + if ( from ) { + ret = color( from( local ) ); + ret[ cache ] = local; + return ret; + } else { + return color( local ); } + }; - if ( this._mouseStarted ) { - this._mouseDrag( event ); - return event.preventDefault(); + // makes red() green() blue() alpha() hue() saturation() lightness() + each( props, function( key, prop ) { + + // alpha is included in more than one space + if ( color.fn[ key ] ) { + return; } + color.fn[ key ] = function( value ) { + var local, cur, match, fn, + vtype = getType( value ); - if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { - this._mouseStarted = - ( this._mouseStart( this._mouseDownEvent, event ) !== false ); - if ( this._mouseStarted ) { - this._mouseDrag( event ); + if ( key === "alpha" ) { + fn = this._hsla ? "hsla" : "rgba"; } else { - this._mouseUp( event ); + fn = spaceName; } - } - - return !this._mouseStarted; - }, - - _mouseUp: function( event ) { - this.document - .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); - - if ( this._mouseStarted ) { - this._mouseStarted = false; + local = this[ fn ](); + cur = local[ prop.idx ]; - if ( event.target === this._mouseDownEvent.target ) { - $.data( event.target, this.widgetName + ".preventClickEvent", true ); + if ( vtype === "undefined" ) { + return cur; } - this._mouseStop( event ); - } - - if ( this._mouseDelayTimer ) { - clearTimeout( this._mouseDelayTimer ); - delete this._mouseDelayTimer; - } + if ( vtype === "function" ) { + value = value.call( this, cur ); + vtype = getType( value ); + } + if ( value == null && prop.empty ) { + return this; + } + if ( vtype === "string" ) { + match = rplusequals.exec( value ); + if ( match ) { + value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); + } + } + local[ prop.idx ] = value; + return this[ fn ]( local ); + }; + } ); +} ); - this.ignoreMissingWhich = false; - mouseHandled = false; - event.preventDefault(); - }, +// add cssHook and .fx.step function for each named hook. +// accept a space separated string of properties +color.hook = function( hook ) { + var hooks = hook.split( " " ); + each( hooks, function( _i, hook ) { + jQuery.cssHooks[ hook ] = { + set: function( elem, value ) { + var parsed, curElem, + backgroundColor = ""; - _mouseDistanceMet: function( event ) { - return ( Math.max( - Math.abs( this._mouseDownEvent.pageX - event.pageX ), - Math.abs( this._mouseDownEvent.pageY - event.pageY ) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function( /* event */ ) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function( /* event */ ) {}, - _mouseDrag: function( /* event */ ) {}, - _mouseStop: function( /* event */ ) {}, - _mouseCapture: function( /* event */ ) { - return true; - } -} ); - - - -// $.ui.plugin is deprecated. Use $.widget() extensions instead. -var plugin = $.ui.plugin = { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args, allowDisconnected ) { - var i, - set = instance.plugins[ name ]; + if ( value !== "transparent" && ( getType( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { + value = color( parsed || value ); + if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { + curElem = hook === "backgroundColor" ? elem.parentNode : elem; + while ( + ( backgroundColor === "" || backgroundColor === "transparent" ) && + curElem && curElem.style + ) { + try { + backgroundColor = jQuery.css( curElem, "backgroundColor" ); + curElem = curElem.parentNode; + } catch ( e ) { + } + } - if ( !set ) { - return; - } + value = value.blend( backgroundColor && backgroundColor !== "transparent" ? + backgroundColor : + "_default" ); + } - if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || - instance.element[ 0 ].parentNode.nodeType === 11 ) ) { - return; - } + value = value.toRgbaString(); + } + try { + elem.style[ hook ] = value; + } catch ( e ) { - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); + // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' + } } - } - } -}; - - + }; + jQuery.fx.step[ hook ] = function( fx ) { + if ( !fx.colorInit ) { + fx.start = color( fx.elem, hook ); + fx.end = color( fx.end ); + fx.colorInit = true; + } + jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); + }; + } ); -var safeActiveElement = $.ui.safeActiveElement = function( document ) { - var activeElement; +}; - // Support: IE 9 only - // IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe> - try { - activeElement = document.activeElement; - } catch ( error ) { - activeElement = document.body; - } +color.hook( stepHooks ); - // Support: IE 9 - 11 only - // IE may return null instead of an element - // Interestingly, this only seems to occur when NOT in an iframe - if ( !activeElement ) { - activeElement = document.body; - } +jQuery.cssHooks.borderColor = { + expand: function( value ) { + var expanded = {}; - // Support: IE 11 only - // IE11 returns a seemingly empty object in some cases when accessing - // document.activeElement from an <iframe> - if ( !activeElement.nodeName ) { - activeElement = document.body; + each( [ "Top", "Right", "Bottom", "Left" ], function( _i, part ) { + expanded[ "border" + part + "Color" ] = value; + } ); + return expanded; } - - return activeElement; }; +// Basic color names only. +// Usage of any of the other color names requires adding yourself or including +// jquery.color.svg-names.js. +colors = jQuery.Color.names = { + // 4.1. Basic color keywords + aqua: "#00ffff", + black: "#000000", + blue: "#0000ff", + fuchsia: "#ff00ff", + gray: "#808080", + green: "#008000", + lime: "#00ff00", + maroon: "#800000", + navy: "#000080", + olive: "#808000", + purple: "#800080", + red: "#ff0000", + silver: "#c0c0c0", + teal: "#008080", + white: "#ffffff", + yellow: "#ffff00", -var safeBlur = $.ui.safeBlur = function( element ) { + // 4.2.3. "transparent" color keyword + transparent: [ null, null, null, 0 ], - // Support: IE9 - 10 only - // If the <body> is blurred, IE will switch windows, see #9420 - if ( element && element.nodeName.toLowerCase() !== "body" ) { - $( element ).trigger( "blur" ); - } + _default: "#ffffff" }; /*! - * jQuery UI Draggable 1.13.1 + * jQuery UI Effects 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -2036,1234 +2055,1333 @@ var safeBlur = $.ui.safeBlur = function( element ) { * http://jquery.org/license */ -//>>label: Draggable -//>>group: Interactions -//>>description: Enables dragging functionality for any element. -//>>docs: http://api.jqueryui.com/draggable/ -//>>demos: http://jqueryui.com/draggable/ -//>>css.structure: ../../themes/base/draggable.css - +//>>label: Effects Core +//>>group: Effects +/* eslint-disable max-len */ +//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/category/effects-core/ +//>>demos: http://jqueryui.com/effect/ -$.widget( "ui.draggable", $.ui.mouse, { - version: "1.13.1", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - // Callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { +var dataSpace = "ui-effects-", + dataSpaceStyle = "ui-effects-style", + dataSpaceAnimated = "ui-effects-animated"; - if ( this.options.helper === "original" ) { - this._setPositionRelative(); - } - if ( this.options.addClasses ) { - this._addClass( "ui-draggable" ); - } - this._setHandleClassName(); +$.effects = { + effect: {} +}; - this._mouseInit(); - }, +/******************************************************************************/ +/****************************** CLASS ANIMATIONS ******************************/ +/******************************************************************************/ +( function() { - _setOption: function( key, value ) { - this._super( key, value ); - if ( key === "handle" ) { - this._removeHandleClassName(); - this._setHandleClassName(); - } - }, +var classAnimationActions = [ "add", "remove", "toggle" ], + shorthandStyles = { + border: 1, + borderBottom: 1, + borderColor: 1, + borderLeft: 1, + borderRight: 1, + borderTop: 1, + borderWidth: 1, + margin: 1, + padding: 1 + }; - _destroy: function() { - if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { - this.destroyOnClear = true; - return; - } - this._removeHandleClassName(); - this._mouseDestroy(); - }, +$.each( + [ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], + function( _, prop ) { + $.fx.step[ prop ] = function( fx ) { + if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) { + jQuery.style( fx.elem, prop, fx.end ); + fx.setAttr = true; + } + }; + } +); - _mouseCapture: function( event ) { - var o = this.options; +function camelCase( string ) { + return string.replace( /-([\da-z])/gi, function( all, letter ) { + return letter.toUpperCase(); + } ); +} - // Among others, prevent a drag on a resizable-handle - if ( this.helper || o.disabled || - $( event.target ).closest( ".ui-resizable-handle" ).length > 0 ) { - return false; +function getElementStyles( elem ) { + var key, len, + style = elem.ownerDocument.defaultView ? + elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : + elem.currentStyle, + styles = {}; + + if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { + len = style.length; + while ( len-- ) { + key = style[ len ]; + if ( typeof style[ key ] === "string" ) { + styles[ camelCase( key ) ] = style[ key ]; + } } - //Quit if we're not on a valid handle - this.handle = this._getHandle( event ); - if ( !this.handle ) { - return false; + // Support: Opera, IE <9 + } else { + for ( key in style ) { + if ( typeof style[ key ] === "string" ) { + styles[ key ] = style[ key ]; + } } + } - this._blurActiveElement( event ); + return styles; +} - this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix ); +function styleDifference( oldStyle, newStyle ) { + var diff = {}, + name, value; - return true; + for ( name in newStyle ) { + value = newStyle[ name ]; + if ( oldStyle[ name ] !== value ) { + if ( !shorthandStyles[ name ] ) { + if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) { + diff[ name ] = value; + } + } + } + } - }, + return diff; +} - _blockFrames: function( selector ) { - this.iframeBlocks = this.document.find( selector ).map( function() { - var iframe = $( this ); +// Support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} - return $( "<div>" ) - .css( "position", "absolute" ) - .appendTo( iframe.parent() ) - .outerWidth( iframe.outerWidth() ) - .outerHeight( iframe.outerHeight() ) - .offset( iframe.offset() )[ 0 ]; - } ); - }, +$.effects.animateClass = function( value, duration, easing, callback ) { + var o = $.speed( duration, easing, callback ); - _unblockFrames: function() { - if ( this.iframeBlocks ) { - this.iframeBlocks.remove(); - delete this.iframeBlocks; - } - }, + return this.queue( function() { + var animated = $( this ), + baseClass = animated.attr( "class" ) || "", + applyClassChange, + allAnimations = o.children ? animated.find( "*" ).addBack() : animated; - _blurActiveElement: function( event ) { - var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ), - target = $( event.target ); + // Map the animated objects to store the original styles. + allAnimations = allAnimations.map( function() { + var el = $( this ); + return { + el: el, + start: getElementStyles( this ) + }; + } ); - // Don't blur if the event occurred on an element that is within - // the currently focused element - // See #10527, #12472 - if ( target.closest( activeElement ).length ) { - return; - } + // Apply class change + applyClassChange = function() { + $.each( classAnimationActions, function( i, action ) { + if ( value[ action ] ) { + animated[ action + "Class" ]( value[ action ] ); + } + } ); + }; + applyClassChange(); - // Blur any element that currently has focus, see #4261 - $.ui.safeBlur( activeElement ); - }, + // Map all animated objects again - calculate new styles and diff + allAnimations = allAnimations.map( function() { + this.end = getElementStyles( this.el[ 0 ] ); + this.diff = styleDifference( this.start, this.end ); + return this; + } ); - _mouseStart: function( event ) { + // Apply original class + animated.attr( "class", baseClass ); - var o = this.options; + // Map all animated objects again - this time collecting a promise + allAnimations = allAnimations.map( function() { + var styleInfo = this, + dfd = $.Deferred(), + opts = $.extend( {}, o, { + queue: false, + complete: function() { + dfd.resolve( styleInfo ); + } + } ); - //Create and append the visible helper - this.helper = this._createHelper( event ); + this.el.animate( this.diff, opts ); + return dfd.promise(); + } ); - this._addClass( this.helper, "ui-draggable-dragging" ); + // Once all animations have completed: + $.when.apply( $, allAnimations.get() ).done( function() { - //Cache the helper size - this._cacheHelperProportions(); + // Set the final class + applyClassChange(); - //If ddmanager is used for droppables, set the global draggable - if ( $.ui.ddmanager ) { - $.ui.ddmanager.current = this; - } + // For each animated element, + // clear all css properties that were animated + $.each( arguments, function() { + var el = this.el; + $.each( this.diff, function( key ) { + el.css( key, "" ); + } ); + } ); - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ + // This is guarnteed to be there if you use jQuery.speed() + // it also handles dequeuing the next anim... + o.complete.call( animated[ 0 ] ); + } ); + } ); +}; - //Cache the margins of the original element - this._cacheMargins(); +$.fn.extend( { + addClass: ( function( orig ) { + return function( classNames, speed, easing, callback ) { + return speed ? + $.effects.animateClass.call( this, + { add: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + } )( $.fn.addClass ), - //Store the helper's css position - this.cssPosition = this.helper.css( "position" ); - this.scrollParent = this.helper.scrollParent( true ); - this.offsetParent = this.helper.offsetParent(); - this.hasFixedAncestor = this.helper.parents().filter( function() { - return $( this ).css( "position" ) === "fixed"; - } ).length > 0; + removeClass: ( function( orig ) { + return function( classNames, speed, easing, callback ) { + return arguments.length > 1 ? + $.effects.animateClass.call( this, + { remove: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + } )( $.fn.removeClass ), - //The element's absolute position on the page minus margins - this.positionAbs = this.element.offset(); - this._refreshOffsets( event ); + toggleClass: ( function( orig ) { + return function( classNames, force, speed, easing, callback ) { + if ( typeof force === "boolean" || force === undefined ) { + if ( !speed ) { - //Generate the original position - this.originalPosition = this.position = this._generatePosition( event, false ); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; + // Without speed parameter + return orig.apply( this, arguments ); + } else { + return $.effects.animateClass.call( this, + ( force ? { add: classNames } : { remove: classNames } ), + speed, easing, callback ); + } + } else { - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - if ( o.cursorAt ) { - this._adjustOffsetFromHelper( o.cursorAt ); - } + // Without force parameter + return $.effects.animateClass.call( this, + { toggle: classNames }, force, speed, easing ); + } + }; + } )( $.fn.toggleClass ), - //Set a containment if given in the options - this._setContainment(); + switchClass: function( remove, add, speed, easing, callback ) { + return $.effects.animateClass.call( this, { + add: add, + remove: remove + }, speed, easing, callback ); + } +} ); - //Trigger event + callbacks - if ( this._trigger( "start", event ) === false ) { - this._clear(); - return false; - } +} )(); - //Recache the helper size - this._cacheHelperProportions(); +/******************************************************************************/ +/*********************************** EFFECTS **********************************/ +/******************************************************************************/ - //Prepare the droppable offsets - if ( $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); - } +( function() { - // Execute the drag once - this causes the helper not to be visible before getting its - // correct position - this._mouseDrag( event, true ); +if ( $.expr && $.expr.pseudos && $.expr.pseudos.animated ) { + $.expr.pseudos.animated = ( function( orig ) { + return function( elem ) { + return !!$( elem ).data( dataSpaceAnimated ) || orig( elem ); + }; + } )( $.expr.pseudos.animated ); +} - // If the ddmanager is used for droppables, inform the manager that dragging has started - // (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStart( this, event ); - } - - return true; - }, - - _refreshOffsets: function( event ) { - this.offset = { - top: this.positionAbs.top - this.margins.top, - left: this.positionAbs.left - this.margins.left, - scroll: false, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() - }; +if ( $.uiBackCompat !== false ) { + $.extend( $.effects, { - this.offset.click = { - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }; - }, + // Saves a set of properties in a data storage + save: function( element, set ) { + var i = 0, length = set.length; + for ( ; i < length; i++ ) { + if ( set[ i ] !== null ) { + element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] ); + } + } + }, - _mouseDrag: function( event, noPropagation ) { + // Restores a set of previously saved properties from a data storage + restore: function( element, set ) { + var val, i = 0, length = set.length; + for ( ; i < length; i++ ) { + if ( set[ i ] !== null ) { + val = element.data( dataSpace + set[ i ] ); + element.css( set[ i ], val ); + } + } + }, - // reset any necessary cached properties (see #5009) - if ( this.hasFixedAncestor ) { - this.offset.parent = this._getParentOffset(); - } + setMode: function( el, mode ) { + if ( mode === "toggle" ) { + mode = el.is( ":hidden" ) ? "show" : "hide"; + } + return mode; + }, - //Compute the helpers position - this.position = this._generatePosition( event, true ); - this.positionAbs = this._convertPositionTo( "absolute" ); + // Wraps the element around a wrapper that copies position properties + createWrapper: function( element ) { - //Call plugins and callbacks and use the resulting position if something is returned - if ( !noPropagation ) { - var ui = this._uiHash(); - if ( this._trigger( "drag", event, ui ) === false ) { - this._mouseUp( new $.Event( "mouseup", event ) ); - return false; + // If the element is already wrapped, return it + if ( element.parent().is( ".ui-effects-wrapper" ) ) { + return element.parent(); } - this.position = ui.position; - } - this.helper[ 0 ].style.left = this.position.left + "px"; - this.helper[ 0 ].style.top = this.position.top + "px"; + // Wrap the element + var props = { + width: element.outerWidth( true ), + height: element.outerHeight( true ), + "float": element.css( "float" ) + }, + wrapper = $( "<div></div>" ) + .addClass( "ui-effects-wrapper" ) + .css( { + fontSize: "100%", + background: "transparent", + border: "none", + margin: 0, + padding: 0 + } ), - if ( $.ui.ddmanager ) { - $.ui.ddmanager.drag( this, event ); - } + // Store the size in case width/height are defined in % - Fixes #5245 + size = { + width: element.width(), + height: element.height() + }, + active = document.activeElement; - return false; - }, + // Support: Firefox + // Firefox incorrectly exposes anonymous content + // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 + try { + // eslint-disable-next-line no-unused-expressions + active.id; + } catch ( e ) { + active = document.body; + } - _mouseStop: function( event ) { + element.wrap( wrapper ); - //If we are using droppables, inform the manager about the drop - var that = this, - dropped = false; - if ( $.ui.ddmanager && !this.options.dropBehaviour ) { - dropped = $.ui.ddmanager.drop( this, event ); - } + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).trigger( "focus" ); + } - //if a drop comes from outside (a sortable) - if ( this.dropped ) { - dropped = this.dropped; - this.dropped = false; - } + // Hotfix for jQuery 1.4 since some change in wrap() seems to actually + // lose the reference to the wrapped element + wrapper = element.parent(); - if ( ( this.options.revert === "invalid" && !dropped ) || - ( this.options.revert === "valid" && dropped ) || - this.options.revert === true || ( typeof this.options.revert === "function" && - this.options.revert.call( this.element, dropped ) ) - ) { - $( this.helper ).animate( - this.originalPosition, - parseInt( this.options.revertDuration, 10 ), - function() { - if ( that._trigger( "stop", event ) !== false ) { - that._clear(); + // Transfer positioning properties to the wrapper + if ( element.css( "position" ) === "static" ) { + wrapper.css( { position: "relative" } ); + element.css( { position: "relative" } ); + } else { + $.extend( props, { + position: element.css( "position" ), + zIndex: element.css( "z-index" ) + } ); + $.each( [ "top", "left", "bottom", "right" ], function( i, pos ) { + props[ pos ] = element.css( pos ); + if ( isNaN( parseInt( props[ pos ], 10 ) ) ) { + props[ pos ] = "auto"; } - } - ); - } else { - if ( this._trigger( "stop", event ) !== false ) { - this._clear(); + } ); + element.css( { + position: "relative", + top: 0, + left: 0, + right: "auto", + bottom: "auto" + } ); } - } + element.css( size ); - return false; - }, + return wrapper.css( props ).show(); + }, - _mouseUp: function( event ) { - this._unblockFrames(); + removeWrapper: function( element ) { + var active = document.activeElement; - // If the ddmanager is used for droppables, inform the manager that dragging has stopped - // (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStop( this, event ); + if ( element.parent().is( ".ui-effects-wrapper" ) ) { + element.parent().replaceWith( element ); + + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).trigger( "focus" ); + } + } + + return element; } + } ); +} - // Only need to focus if the event occurred on the draggable itself, see #10527 - if ( this.handleElement.is( event.target ) ) { +$.extend( $.effects, { + version: "1.13.2", - // The interaction is over; whether or not the click resulted in a drag, - // focus the element - this.element.trigger( "focus" ); + define: function( name, mode, effect ) { + if ( !effect ) { + effect = mode; + mode = "effect"; } - return $.ui.mouse.prototype._mouseUp.call( this, event ); - }, + $.effects.effect[ name ] = effect; + $.effects.effect[ name ].mode = mode; - cancel: function() { + return effect; + }, - if ( this.helper.is( ".ui-draggable-dragging" ) ) { - this._mouseUp( new $.Event( "mouseup", { target: this.element[ 0 ] } ) ); - } else { - this._clear(); + scaledDimensions: function( element, percent, direction ) { + if ( percent === 0 ) { + return { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + }; } - return this; + var x = direction !== "horizontal" ? ( ( percent || 100 ) / 100 ) : 1, + y = direction !== "vertical" ? ( ( percent || 100 ) / 100 ) : 1; - }, + return { + height: element.height() * y, + width: element.width() * x, + outerHeight: element.outerHeight() * y, + outerWidth: element.outerWidth() * x + }; - _getHandle: function( event ) { - return this.options.handle ? - !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : - true; }, - _setHandleClassName: function() { - this.handleElement = this.options.handle ? - this.element.find( this.options.handle ) : this.element; - this._addClass( this.handleElement, "ui-draggable-handle" ); + clipToBox: function( animation ) { + return { + width: animation.clip.right - animation.clip.left, + height: animation.clip.bottom - animation.clip.top, + left: animation.clip.left, + top: animation.clip.top + }; }, - _removeHandleClassName: function() { - this._removeClass( this.handleElement, "ui-draggable-handle" ); + // Injects recently queued functions to be first in line (after "inprogress") + unshift: function( element, queueLength, count ) { + var queue = element.queue(); + + if ( queueLength > 1 ) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queueLength, count ) ) ); + } + element.dequeue(); }, - _createHelper: function( event ) { + saveStyle: function( element ) { + element.data( dataSpaceStyle, element[ 0 ].style.cssText ); + }, - var o = this.options, - helperIsFunction = typeof o.helper === "function", - helper = helperIsFunction ? - $( o.helper.apply( this.element[ 0 ], [ event ] ) ) : - ( o.helper === "clone" ? - this.element.clone().removeAttr( "id" ) : - this.element ); + restoreStyle: function( element ) { + element[ 0 ].style.cssText = element.data( dataSpaceStyle ) || ""; + element.removeData( dataSpaceStyle ); + }, - if ( !helper.parents( "body" ).length ) { - helper.appendTo( ( o.appendTo === "parent" ? - this.element[ 0 ].parentNode : - o.appendTo ) ); - } + mode: function( element, mode ) { + var hidden = element.is( ":hidden" ); - // Http://bugs.jqueryui.com/ticket/9446 - // a helper function can return the original element - // which wouldn't have been set to relative in _create - if ( helperIsFunction && helper[ 0 ] === this.element[ 0 ] ) { - this._setPositionRelative(); + if ( mode === "toggle" ) { + mode = hidden ? "show" : "hide"; } - - if ( helper[ 0 ] !== this.element[ 0 ] && - !( /(fixed|absolute)/ ).test( helper.css( "position" ) ) ) { - helper.css( "position", "absolute" ); + if ( hidden ? mode === "hide" : mode === "show" ) { + mode = "none"; } - - return helper; - + return mode; }, - _setPositionRelative: function() { - if ( !( /^(?:r|a|f)/ ).test( this.element.css( "position" ) ) ) { - this.element[ 0 ].style.position = "relative"; - } - }, + // Translates a [top,left] array into a baseline value + getBaseline: function( origin, original ) { + var y, x; - _adjustOffsetFromHelper: function( obj ) { - if ( typeof obj === "string" ) { - obj = obj.split( " " ); - } - if ( Array.isArray( obj ) ) { - obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; - } - if ( "left" in obj ) { - this.offset.click.left = obj.left + this.margins.left; - } - if ( "right" in obj ) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ( "top" in obj ) { - this.offset.click.top = obj.top + this.margins.top; + switch ( origin[ 0 ] ) { + case "top": + y = 0; + break; + case "middle": + y = 0.5; + break; + case "bottom": + y = 1; + break; + default: + y = origin[ 0 ] / original.height; } - if ( "bottom" in obj ) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + + switch ( origin[ 1 ] ) { + case "left": + x = 0; + break; + case "center": + x = 0.5; + break; + case "right": + x = 1; + break; + default: + x = origin[ 1 ] / original.width; } - }, - _isRootNode: function( element ) { - return ( /(html|body)/i ).test( element.tagName ) || element === this.document[ 0 ]; + return { + x: x, + y: y + }; }, - _getParentOffset: function() { + // Creates a placeholder element so that the original element can be made absolute + createPlaceholder: function( element ) { + var placeholder, + cssPosition = element.css( "position" ), + position = element.position(); - //Get the offsetParent and cache its position - var po = this.offsetParent.offset(), - document = this.document[ 0 ]; + // Lock in margins first to account for form elements, which + // will change margin if you explicitly set height + // see: http://jsfiddle.net/JZSMt/3/ https://bugs.webkit.org/show_bug.cgi?id=107380 + // Support: Safari + element.css( { + marginTop: element.css( "marginTop" ), + marginBottom: element.css( "marginBottom" ), + marginLeft: element.css( "marginLeft" ), + marginRight: element.css( "marginRight" ) + } ) + .outerWidth( element.outerWidth() ) + .outerHeight( element.outerHeight() ); - // This is a special case where we need to modify a offset calculated on start, since the - // following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the - // next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't - // the document, which means that the scroll is included in the initial calculation of the - // offset of the parent, and never recalculated upon drag - if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== document && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } + if ( /^(static|relative)/.test( cssPosition ) ) { + cssPosition = "absolute"; - if ( this._isRootNode( this.offsetParent[ 0 ] ) ) { - po = { top: 0, left: 0 }; - } + placeholder = $( "<" + element[ 0 ].nodeName + ">" ).insertAfter( element ).css( { - return { - top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), - left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) - }; + // Convert inline to inline block to account for inline elements + // that turn to inline block based on content (like img) + display: /^(inline|ruby)/.test( element.css( "display" ) ) ? + "inline-block" : + "block", + visibility: "hidden", - }, + // Margins need to be set to account for margin collapse + marginTop: element.css( "marginTop" ), + marginBottom: element.css( "marginBottom" ), + marginLeft: element.css( "marginLeft" ), + marginRight: element.css( "marginRight" ), + "float": element.css( "float" ) + } ) + .outerWidth( element.outerWidth() ) + .outerHeight( element.outerHeight() ) + .addClass( "ui-effects-placeholder" ); - _getRelativeOffset: function() { - if ( this.cssPosition !== "relative" ) { - return { top: 0, left: 0 }; + element.data( dataSpace + "placeholder", placeholder ); } - var p = this.element.position(), - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); - - return { - top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + - ( !scrollIsRootNode ? this.scrollParent.scrollTop() : 0 ), - left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + - ( !scrollIsRootNode ? this.scrollParent.scrollLeft() : 0 ) - }; + element.css( { + position: cssPosition, + left: position.left, + top: position.top + } ); + return placeholder; }, - _cacheMargins: function() { - this.margins = { - left: ( parseInt( this.element.css( "marginLeft" ), 10 ) || 0 ), - top: ( parseInt( this.element.css( "marginTop" ), 10 ) || 0 ), - right: ( parseInt( this.element.css( "marginRight" ), 10 ) || 0 ), - bottom: ( parseInt( this.element.css( "marginBottom" ), 10 ) || 0 ) - }; + removePlaceholder: function( element ) { + var dataKey = dataSpace + "placeholder", + placeholder = element.data( dataKey ); + + if ( placeholder ) { + placeholder.remove(); + element.removeData( dataKey ); + } }, - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; + // Removes a placeholder if it exists and restores + // properties that were modified during placeholder creation + cleanUp: function( element ) { + $.effects.restoreStyle( element ); + $.effects.removePlaceholder( element ); }, - _setContainment: function() { + setTransition: function( element, list, factor, value ) { + value = value || {}; + $.each( list, function( i, x ) { + var unit = element.cssUnit( x ); + if ( unit[ 0 ] > 0 ) { + value[ x ] = unit[ 0 ] * factor + unit[ 1 ]; + } + } ); + return value; + } +} ); - var isUserScrollable, c, ce, - o = this.options, - document = this.document[ 0 ]; +// Return an effect options object for the given parameters: +function _normalizeArguments( effect, options, speed, callback ) { - this.relativeContainer = null; + // Allow passing all options as the first parameter + if ( $.isPlainObject( effect ) ) { + options = effect; + effect = effect.effect; + } - if ( !o.containment ) { - this.containment = null; - return; - } + // Convert to an object + effect = { effect: effect }; - if ( o.containment === "window" ) { - this.containment = [ - $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, - $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, - $( window ).scrollLeft() + $( window ).width() - - this.helperProportions.width - this.margins.left, - $( window ).scrollTop() + - ( $( window ).height() || document.body.parentNode.scrollHeight ) - - this.helperProportions.height - this.margins.top - ]; - return; - } + // Catch (effect, null, ...) + if ( options == null ) { + options = {}; + } - if ( o.containment === "document" ) { - this.containment = [ - 0, - 0, - $( document ).width() - this.helperProportions.width - this.margins.left, - ( $( document ).height() || document.body.parentNode.scrollHeight ) - - this.helperProportions.height - this.margins.top - ]; - return; - } + // Catch (effect, callback) + if ( typeof options === "function" ) { + callback = options; + speed = null; + options = {}; + } - if ( o.containment.constructor === Array ) { - this.containment = o.containment; - return; - } + // Catch (effect, speed, ?) + if ( typeof options === "number" || $.fx.speeds[ options ] ) { + callback = speed; + speed = options; + options = {}; + } - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } + // Catch (effect, options, callback) + if ( typeof speed === "function" ) { + callback = speed; + speed = null; + } - c = $( o.containment ); - ce = c[ 0 ]; + // Add options to effect + if ( options ) { + $.extend( effect, options ); + } - if ( !ce ) { - return; - } + speed = speed || options.duration; + effect.duration = $.fx.off ? 0 : + typeof speed === "number" ? speed : + speed in $.fx.speeds ? $.fx.speeds[ speed ] : + $.fx.speeds._default; - isUserScrollable = /(scroll|auto)/.test( c.css( "overflow" ) ); + effect.complete = callback || options.complete; - this.containment = [ - ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + - ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), - ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + - ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ), - ( isUserScrollable ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - - this.helperProportions.width - - this.margins.left - - this.margins.right, - ( isUserScrollable ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - - this.helperProportions.height - - this.margins.top - - this.margins.bottom - ]; - this.relativeContainer = c; - }, + return effect; +} - _convertPositionTo: function( d, pos ) { +function standardAnimationOption( option ) { - if ( !pos ) { - pos = this.position; - } + // Valid standard speeds (nothing, number, named speed) + if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) { + return true; + } - var mod = d === "absolute" ? 1 : -1, - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); + // Invalid strings - treat as "normal" speed + if ( typeof option === "string" && !$.effects.effect[ option ] ) { + return true; + } - return { - top: ( + // Complete callback + if ( typeof option === "function" ) { + return true; + } - // The absolute mouse position - pos.top + + // Options hash (but not naming an effect) + if ( typeof option === "object" && !option.effect ) { + return true; + } - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top * mod + + // Didn't match any standard API + return false; +} - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top * mod - - ( ( this.cssPosition === "fixed" ? - -this.offset.scroll.top : - ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) * mod ) - ), - left: ( +$.fn.extend( { + effect: function( /* effect, options, speed, callback */ ) { + var args = _normalizeArguments.apply( this, arguments ), + effectMethod = $.effects.effect[ args.effect ], + defaultMode = effectMethod.mode, + queue = args.queue, + queueName = queue || "fx", + complete = args.complete, + mode = args.mode, + modes = [], + prefilter = function( next ) { + var el = $( this ), + normalizedMode = $.effects.mode( el, mode ) || defaultMode; - // The absolute mouse position - pos.left + + // Sentinel for duck-punching the :animated pseudo-selector + el.data( dataSpaceAnimated, true ); - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left * mod + + // Save effect mode for later use, + // we can't just call $.effects.mode again later, + // as the .show() below destroys the initial state + modes.push( normalizedMode ); - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left * mod - - ( ( this.cssPosition === "fixed" ? - -this.offset.scroll.left : - ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) * mod ) - ) - }; + // See $.uiBackCompat inside of run() for removal of defaultMode in 1.14 + if ( defaultMode && ( normalizedMode === "show" || + ( normalizedMode === defaultMode && normalizedMode === "hide" ) ) ) { + el.show(); + } - }, + if ( !defaultMode || normalizedMode !== "none" ) { + $.effects.saveStyle( el ); + } - _generatePosition: function( event, constrainPosition ) { + if ( typeof next === "function" ) { + next(); + } + }; - var containment, co, top, left, - o = this.options, - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ), - pageX = event.pageX, - pageY = event.pageY; + if ( $.fx.off || !effectMethod ) { - // Cache the scroll - if ( !scrollIsRootNode || !this.offset.scroll ) { - this.offset.scroll = { - top: this.scrollParent.scrollTop(), - left: this.scrollParent.scrollLeft() - }; + // Delegate to the original method (e.g., .show()) if possible + if ( mode ) { + return this[ mode ]( args.duration, complete ); + } else { + return this.each( function() { + if ( complete ) { + complete.call( this ); + } + } ); + } } - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ + function run( next ) { + var elem = $( this ); - // If we are not dragging yet, we won't check for options - if ( constrainPosition ) { - if ( this.containment ) { - if ( this.relativeContainer ) { - co = this.relativeContainer.offset(); - containment = [ - this.containment[ 0 ] + co.left, - this.containment[ 1 ] + co.top, - this.containment[ 2 ] + co.left, - this.containment[ 3 ] + co.top - ]; - } else { - containment = this.containment; - } + function cleanup() { + elem.removeData( dataSpaceAnimated ); - if ( event.pageX - this.offset.click.left < containment[ 0 ] ) { - pageX = containment[ 0 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top < containment[ 1 ] ) { - pageY = containment[ 1 ] + this.offset.click.top; + $.effects.cleanUp( elem ); + + if ( args.mode === "hide" ) { + elem.hide(); } - if ( event.pageX - this.offset.click.left > containment[ 2 ] ) { - pageX = containment[ 2 ] + this.offset.click.left; + + done(); + } + + function done() { + if ( typeof complete === "function" ) { + complete.call( elem[ 0 ] ); } - if ( event.pageY - this.offset.click.top > containment[ 3 ] ) { - pageY = containment[ 3 ] + this.offset.click.top; + + if ( typeof next === "function" ) { + next(); } } - if ( o.grid ) { - - //Check for grid elements set to 0 to prevent divide by 0 error causing invalid - // argument errors in IE (see ticket #6950) - top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY - - this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY; - pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] || - top - this.offset.click.top > containment[ 3 ] ) ? - top : - ( ( top - this.offset.click.top >= containment[ 1 ] ) ? - top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top; + // Override mode option on a per element basis, + // as toggle can be either show or hide depending on element state + args.mode = modes.shift(); - left = o.grid[ 0 ] ? this.originalPageX + - Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] : - this.originalPageX; - pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] || - left - this.offset.click.left > containment[ 2 ] ) ? - left : - ( ( left - this.offset.click.left >= containment[ 0 ] ) ? - left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left; - } + if ( $.uiBackCompat !== false && !defaultMode ) { + if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { - if ( o.axis === "y" ) { - pageX = this.originalPageX; - } + // Call the core method to track "olddisplay" properly + elem[ mode ](); + done(); + } else { + effectMethod.call( elem[ 0 ], args, done ); + } + } else { + if ( args.mode === "none" ) { - if ( o.axis === "x" ) { - pageY = this.originalPageY; + // Call the core method to track "olddisplay" properly + elem[ mode ](); + done(); + } else { + effectMethod.call( elem[ 0 ], args, cleanup ); + } } } - return { - top: ( - - // The absolute mouse position - pageY - - - // Click offset (relative to the element) - this.offset.click.top - - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top - - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top + - ( this.cssPosition === "fixed" ? - -this.offset.scroll.top : - ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) - ), - left: ( - - // The absolute mouse position - pageX - + // Run prefilter on all elements first to ensure that + // any showing or hiding happens before placeholder creation, + // which ensures that any layout changes are correctly captured. + return queue === false ? + this.each( prefilter ).each( run ) : + this.queue( queueName, prefilter ).queue( queueName, run ); + }, - // Click offset (relative to the element) - this.offset.click.left - + show: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "show"; + return this.effect.call( this, args ); + } + }; + } )( $.fn.show ), - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left - + hide: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "hide"; + return this.effect.call( this, args ); + } + }; + } )( $.fn.hide ), - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left + - ( this.cssPosition === "fixed" ? - -this.offset.scroll.left : - ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) - ) + toggle: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) || typeof option === "boolean" ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "toggle"; + return this.effect.call( this, args ); + } }; + } )( $.fn.toggle ), + cssUnit: function( key ) { + var style = this.css( key ), + val = []; + + $.each( [ "em", "px", "%", "pt" ], function( i, unit ) { + if ( style.indexOf( unit ) > 0 ) { + val = [ parseFloat( style ), unit ]; + } + } ); + return val; }, - _clear: function() { - this._removeClass( this.helper, "ui-draggable-dragging" ); - if ( this.helper[ 0 ] !== this.element[ 0 ] && !this.cancelHelperRemoval ) { - this.helper.remove(); - } - this.helper = null; - this.cancelHelperRemoval = false; - if ( this.destroyOnClear ) { - this.destroy(); + cssClip: function( clipObj ) { + if ( clipObj ) { + return this.css( "clip", "rect(" + clipObj.top + "px " + clipObj.right + "px " + + clipObj.bottom + "px " + clipObj.left + "px)" ); } + return parseClip( this.css( "clip" ), this ); }, - // From now on bulk stuff - mainly helpers - - _trigger: function( type, event, ui ) { - ui = ui || this._uiHash(); - $.ui.plugin.call( this, type, [ event, ui, this ], true ); + transfer: function( options, done ) { + var element = $( this ), + target = $( options.to ), + targetFixed = target.css( "position" ) === "fixed", + body = $( "body" ), + fixTop = targetFixed ? body.scrollTop() : 0, + fixLeft = targetFixed ? body.scrollLeft() : 0, + endPosition = target.offset(), + animation = { + top: endPosition.top - fixTop, + left: endPosition.left - fixLeft, + height: target.innerHeight(), + width: target.innerWidth() + }, + startPosition = element.offset(), + transfer = $( "<div class='ui-effects-transfer'></div>" ); - // Absolute position and offset (see #6884 ) have to be recalculated after plugins - if ( /^(drag|start|stop)/.test( type ) ) { - this.positionAbs = this._convertPositionTo( "absolute" ); - ui.offset = this.positionAbs; - } - return $.Widget.prototype._trigger.call( this, type, event, ui ); - }, + transfer + .appendTo( "body" ) + .addClass( options.className ) + .css( { + top: startPosition.top - fixTop, + left: startPosition.left - fixLeft, + height: element.innerHeight(), + width: element.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + } ) + .animate( animation, options.duration, options.easing, function() { + transfer.remove(); + if ( typeof done === "function" ) { + done(); + } + } ); + } +} ); - plugins: {}, +function parseClip( str, element ) { + var outerWidth = element.outerWidth(), + outerHeight = element.outerHeight(), + clipRegex = /^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/, + values = clipRegex.exec( str ) || [ "", 0, outerWidth, outerHeight, 0 ]; - _uiHash: function() { return { - helper: this.helper, - position: this.position, - originalPosition: this.originalPosition, - offset: this.positionAbs + top: parseFloat( values[ 1 ] ) || 0, + right: values[ 2 ] === "auto" ? outerWidth : parseFloat( values[ 2 ] ), + bottom: values[ 3 ] === "auto" ? outerHeight : parseFloat( values[ 3 ] ), + left: parseFloat( values[ 4 ] ) || 0 }; +} + +$.fx.step.clip = function( fx ) { + if ( !fx.clipInit ) { + fx.start = $( fx.elem ).cssClip(); + if ( typeof fx.end === "string" ) { + fx.end = parseClip( fx.end, fx.elem ); + } + fx.clipInit = true; } -} ); + $( fx.elem ).cssClip( { + top: fx.pos * ( fx.end.top - fx.start.top ) + fx.start.top, + right: fx.pos * ( fx.end.right - fx.start.right ) + fx.start.right, + bottom: fx.pos * ( fx.end.bottom - fx.start.bottom ) + fx.start.bottom, + left: fx.pos * ( fx.end.left - fx.start.left ) + fx.start.left + } ); +}; -$.ui.plugin.add( "draggable", "connectToSortable", { - start: function( event, ui, draggable ) { - var uiSortable = $.extend( {}, ui, { - item: draggable.element - } ); +} )(); - draggable.sortables = []; - $( draggable.options.connectToSortable ).each( function() { - var sortable = $( this ).sortable( "instance" ); +/******************************************************************************/ +/*********************************** EASING ***********************************/ +/******************************************************************************/ - if ( sortable && !sortable.options.disabled ) { - draggable.sortables.push( sortable ); +( function() { - // RefreshPositions is called at drag start to refresh the containerCache - // which is used in drag. This ensures it's initialized and synchronized - // with any changes that might have happened on the page since initialization. - sortable.refreshPositions(); - sortable._trigger( "activate", event, uiSortable ); - } - } ); - }, - stop: function( event, ui, draggable ) { - var uiSortable = $.extend( {}, ui, { - item: draggable.element - } ); +// Based on easing equations from Robert Penner (http://www.robertpenner.com/easing) - draggable.cancelHelperRemoval = false; +var baseEasings = {}; - $.each( draggable.sortables, function() { - var sortable = this; +$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) { + baseEasings[ name ] = function( p ) { + return Math.pow( p, i + 2 ); + }; +} ); - if ( sortable.isOver ) { - sortable.isOver = 0; +$.extend( baseEasings, { + Sine: function( p ) { + return 1 - Math.cos( p * Math.PI / 2 ); + }, + Circ: function( p ) { + return 1 - Math.sqrt( 1 - p * p ); + }, + Elastic: function( p ) { + return p === 0 || p === 1 ? p : + -Math.pow( 2, 8 * ( p - 1 ) ) * Math.sin( ( ( p - 1 ) * 80 - 7.5 ) * Math.PI / 15 ); + }, + Back: function( p ) { + return p * p * ( 3 * p - 2 ); + }, + Bounce: function( p ) { + var pow2, + bounce = 4; - // Allow this sortable to handle removing the helper - draggable.cancelHelperRemoval = true; - sortable.cancelHelperRemoval = false; + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); + } +} ); - // Use _storedCSS To restore properties in the sortable, - // as this also handles revert (#9675) since the draggable - // may have modified them in unexpected ways (#8809) - sortable._storedCSS = { - position: sortable.placeholder.css( "position" ), - top: sortable.placeholder.css( "top" ), - left: sortable.placeholder.css( "left" ) - }; +$.each( baseEasings, function( name, easeIn ) { + $.easing[ "easeIn" + name ] = easeIn; + $.easing[ "easeOut" + name ] = function( p ) { + return 1 - easeIn( 1 - p ); + }; + $.easing[ "easeInOut" + name ] = function( p ) { + return p < 0.5 ? + easeIn( p * 2 ) / 2 : + 1 - easeIn( p * -2 + 2 ) / 2; + }; +} ); - sortable._mouseStop( event ); +} )(); - // Once drag has ended, the sortable should return to using - // its original helper, not the shared helper from draggable - sortable.options.helper = sortable.options._helper; - } else { +var effect = $.effects; - // Prevent this Sortable from removing the helper. - // However, don't set the draggable to remove the helper - // either as another connected Sortable may yet handle the removal. - sortable.cancelHelperRemoval = true; - - sortable._trigger( "deactivate", event, uiSortable ); - } - } ); - }, - drag: function( event, ui, draggable ) { - $.each( draggable.sortables, function() { - var innermostIntersecting = false, - sortable = this; - // Copy over variables that sortable's _intersectsWith uses - sortable.positionAbs = draggable.positionAbs; - sortable.helperProportions = draggable.helperProportions; - sortable.offset.click = draggable.offset.click; +/*! + * jQuery UI Effects Blind 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( sortable._intersectsWith( sortable.containerCache ) ) { - innermostIntersecting = true; +//>>label: Blind Effect +//>>group: Effects +//>>description: Blinds the element. +//>>docs: http://api.jqueryui.com/blind-effect/ +//>>demos: http://jqueryui.com/effect/ - $.each( draggable.sortables, function() { - // Copy over variables that sortable's _intersectsWith uses - this.positionAbs = draggable.positionAbs; - this.helperProportions = draggable.helperProportions; - this.offset.click = draggable.offset.click; +var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, done ) { + var map = { + up: [ "bottom", "top" ], + vertical: [ "bottom", "top" ], + down: [ "top", "bottom" ], + left: [ "right", "left" ], + horizontal: [ "right", "left" ], + right: [ "left", "right" ] + }, + element = $( this ), + direction = options.direction || "up", + start = element.cssClip(), + animate = { clip: $.extend( {}, start ) }, + placeholder = $.effects.createPlaceholder( element ); - if ( this !== sortable && - this._intersectsWith( this.containerCache ) && - $.contains( sortable.element[ 0 ], this.element[ 0 ] ) ) { - innermostIntersecting = false; - } + animate.clip[ map[ direction ][ 0 ] ] = animate.clip[ map[ direction ][ 1 ] ]; - return innermostIntersecting; - } ); - } + if ( options.mode === "show" ) { + element.cssClip( animate.clip ); + if ( placeholder ) { + placeholder.css( $.effects.clipToBox( animate ) ); + } - if ( innermostIntersecting ) { + animate.clip = start; + } - // If it intersects, we use a little isOver variable and set it once, - // so that the move-in stuff gets fired only once. - if ( !sortable.isOver ) { - sortable.isOver = 1; + if ( placeholder ) { + placeholder.animate( $.effects.clipToBox( animate ), options.duration, options.easing ); + } - // Store draggable's parent in case we need to reappend to it later. - draggable._parent = ui.helper.parent(); + element.animate( animate, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); - sortable.currentItem = ui.helper - .appendTo( sortable.element ) - .data( "ui-sortable-item", true ); - // Store helper option to later restore it - sortable.options._helper = sortable.options.helper; +/*! + * jQuery UI Effects Bounce 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - sortable.options.helper = function() { - return ui.helper[ 0 ]; - }; +//>>label: Bounce Effect +//>>group: Effects +//>>description: Bounces an element horizontally or vertically n times. +//>>docs: http://api.jqueryui.com/bounce-effect/ +//>>demos: http://jqueryui.com/effect/ - // Fire the start events of the sortable with our passed browser event, - // and our own helper (so it doesn't create a new one) - event.target = sortable.currentItem[ 0 ]; - sortable._mouseCapture( event, true ); - sortable._mouseStart( event, true, true ); - // Because the browser event is way off the new appended portlet, - // modify necessary variables to reflect the changes - sortable.offset.click.top = draggable.offset.click.top; - sortable.offset.click.left = draggable.offset.click.left; - sortable.offset.parent.left -= draggable.offset.parent.left - - sortable.offset.parent.left; - sortable.offset.parent.top -= draggable.offset.parent.top - - sortable.offset.parent.top; +var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) { + var upAnim, downAnim, refValue, + element = $( this ), - draggable._trigger( "toSortable", event ); + // Defaults: + mode = options.mode, + hide = mode === "hide", + show = mode === "show", + direction = options.direction || "up", + distance = options.distance, + times = options.times || 5, - // Inform draggable that the helper is in a valid drop zone, - // used solely in the revert option to handle "valid/invalid". - draggable.dropped = sortable.element; + // Number of internal animations + anims = times * 2 + ( show || hide ? 1 : 0 ), + speed = options.duration / anims, + easing = options.easing, - // Need to refreshPositions of all sortables in the case that - // adding to one sortable changes the location of the other sortables (#9675) - $.each( draggable.sortables, function() { - this.refreshPositions(); - } ); + // Utility: + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ), + i = 0, - // Hack so receive/update callbacks work (mostly) - draggable.currentItem = draggable.element; - sortable.fromOutside = draggable; - } + queuelen = element.queue().length; - if ( sortable.currentItem ) { - sortable._mouseDrag( event ); + $.effects.createPlaceholder( element ); - // Copy the sortable's position because the draggable's can potentially reflect - // a relative position, while sortable is always absolute, which the dragged - // element has now become. (#8809) - ui.position = sortable.position; - } - } else { + refValue = element.css( ref ); - // If it doesn't intersect with the sortable, and it intersected before, - // we fake the drag stop of the sortable, but make sure it doesn't remove - // the helper by using cancelHelperRemoval. - if ( sortable.isOver ) { + // Default distance for the BIGGEST bounce is the outer Distance / 3 + if ( !distance ) { + distance = element[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3; + } - sortable.isOver = 0; - sortable.cancelHelperRemoval = true; + if ( show ) { + downAnim = { opacity: 1 }; + downAnim[ ref ] = refValue; - // Calling sortable's mouseStop would trigger a revert, - // so revert must be temporarily false until after mouseStop is called. - sortable.options._revert = sortable.options.revert; - sortable.options.revert = false; + // If we are showing, force opacity 0 and set the initial position + // then do the "first" animation + element + .css( "opacity", 0 ) + .css( ref, motion ? -distance * 2 : distance * 2 ) + .animate( downAnim, speed, easing ); + } - sortable._trigger( "out", event, sortable._uiHash( sortable ) ); - sortable._mouseStop( event, true ); + // Start at the smallest distance if we are hiding + if ( hide ) { + distance = distance / Math.pow( 2, times - 1 ); + } - // Restore sortable behaviors that were modfied - // when the draggable entered the sortable area (#9481) - sortable.options.revert = sortable.options._revert; - sortable.options.helper = sortable.options._helper; + downAnim = {}; + downAnim[ ref ] = refValue; - if ( sortable.placeholder ) { - sortable.placeholder.remove(); - } + // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here + for ( ; i < times; i++ ) { + upAnim = {}; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; - // Restore and recalculate the draggable's offset considering the sortable - // may have modified them in unexpected ways. (#8809, #10669) - ui.helper.appendTo( draggable._parent ); - draggable._refreshOffsets( event ); - ui.position = draggable._generatePosition( event, true ); + element + .animate( upAnim, speed, easing ) + .animate( downAnim, speed, easing ); - draggable._trigger( "fromSortable", event ); + distance = hide ? distance * 2 : distance / 2; + } - // Inform draggable that the helper is no longer in a valid drop zone - draggable.dropped = false; + // Last Bounce when Hiding + if ( hide ) { + upAnim = { opacity: 0 }; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; - // Need to refreshPositions of all sortables just in case removing - // from one sortable changes the location of other sortables (#9675) - $.each( draggable.sortables, function() { - this.refreshPositions(); - } ); - } - } - } ); + element.animate( upAnim, speed, easing ); } -} ); -$.ui.plugin.add( "draggable", "cursor", { - start: function( event, ui, instance ) { - var t = $( "body" ), - o = instance.options; + element.queue( done ); - if ( t.css( "cursor" ) ) { - o._cursor = t.css( "cursor" ); - } - t.css( "cursor", o.cursor ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; - if ( o._cursor ) { - $( "body" ).css( "cursor", o._cursor ); - } - } + $.effects.unshift( element, queuelen, anims + 1 ); } ); -$.ui.plugin.add( "draggable", "opacity", { - start: function( event, ui, instance ) { - var t = $( ui.helper ), - o = instance.options; - if ( t.css( "opacity" ) ) { - o._opacity = t.css( "opacity" ); - } - t.css( "opacity", o.opacity ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; - if ( o._opacity ) { - $( ui.helper ).css( "opacity", o._opacity ); - } + +/*! + * jQuery UI Effects Clip 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Clip Effect +//>>group: Effects +//>>description: Clips the element on and off like an old TV. +//>>docs: http://api.jqueryui.com/clip-effect/ +//>>demos: http://jqueryui.com/effect/ + + +var effectsEffectClip = $.effects.define( "clip", "hide", function( options, done ) { + var start, + animate = {}, + element = $( this ), + direction = options.direction || "vertical", + both = direction === "both", + horizontal = both || direction === "horizontal", + vertical = both || direction === "vertical"; + + start = element.cssClip(); + animate.clip = { + top: vertical ? ( start.bottom - start.top ) / 2 : start.top, + right: horizontal ? ( start.right - start.left ) / 2 : start.right, + bottom: vertical ? ( start.bottom - start.top ) / 2 : start.bottom, + left: horizontal ? ( start.right - start.left ) / 2 : start.left + }; + + $.effects.createPlaceholder( element ); + + if ( options.mode === "show" ) { + element.cssClip( animate.clip ); + animate.clip = start; } + + element.animate( animate, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); + } ); -$.ui.plugin.add( "draggable", "scroll", { - start: function( event, ui, i ) { - if ( !i.scrollParentNotHidden ) { - i.scrollParentNotHidden = i.helper.scrollParent( false ); - } - if ( i.scrollParentNotHidden[ 0 ] !== i.document[ 0 ] && - i.scrollParentNotHidden[ 0 ].tagName !== "HTML" ) { - i.overflowOffset = i.scrollParentNotHidden.offset(); - } - }, - drag: function( event, ui, i ) { +/*! + * jQuery UI Effects Drop 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - var o = i.options, - scrolled = false, - scrollParent = i.scrollParentNotHidden[ 0 ], - document = i.document[ 0 ]; +//>>label: Drop Effect +//>>group: Effects +//>>description: Moves an element in one direction and hides it at the same time. +//>>docs: http://api.jqueryui.com/drop-effect/ +//>>demos: http://jqueryui.com/effect/ - if ( scrollParent !== document && scrollParent.tagName !== "HTML" ) { - if ( !o.axis || o.axis !== "x" ) { - if ( ( i.overflowOffset.top + scrollParent.offsetHeight ) - event.pageY < - o.scrollSensitivity ) { - scrollParent.scrollTop = scrolled = scrollParent.scrollTop + o.scrollSpeed; - } else if ( event.pageY - i.overflowOffset.top < o.scrollSensitivity ) { - scrollParent.scrollTop = scrolled = scrollParent.scrollTop - o.scrollSpeed; - } - } - if ( !o.axis || o.axis !== "y" ) { - if ( ( i.overflowOffset.left + scrollParent.offsetWidth ) - event.pageX < - o.scrollSensitivity ) { - scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft + o.scrollSpeed; - } else if ( event.pageX - i.overflowOffset.left < o.scrollSensitivity ) { - scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft - o.scrollSpeed; - } - } +var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, done ) { - } else { + var distance, + element = $( this ), + mode = options.mode, + show = mode === "show", + direction = options.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ) ? "-=" : "+=", + oppositeMotion = ( motion === "+=" ) ? "-=" : "+=", + animation = { + opacity: 0 + }; - if ( !o.axis || o.axis !== "x" ) { - if ( event.pageY - $( document ).scrollTop() < o.scrollSensitivity ) { - scrolled = $( document ).scrollTop( $( document ).scrollTop() - o.scrollSpeed ); - } else if ( $( window ).height() - ( event.pageY - $( document ).scrollTop() ) < - o.scrollSensitivity ) { - scrolled = $( document ).scrollTop( $( document ).scrollTop() + o.scrollSpeed ); - } - } + $.effects.createPlaceholder( element ); - if ( !o.axis || o.axis !== "y" ) { - if ( event.pageX - $( document ).scrollLeft() < o.scrollSensitivity ) { - scrolled = $( document ).scrollLeft( - $( document ).scrollLeft() - o.scrollSpeed - ); - } else if ( $( window ).width() - ( event.pageX - $( document ).scrollLeft() ) < - o.scrollSensitivity ) { - scrolled = $( document ).scrollLeft( - $( document ).scrollLeft() + o.scrollSpeed - ); - } - } + distance = options.distance || + element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ) / 2; - } + animation[ ref ] = motion + distance; - if ( scrolled !== false && $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( i, event ); - } + if ( show ) { + element.css( animation ); + animation[ ref ] = oppositeMotion + distance; + animation.opacity = 1; } + + // Animate + element.animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); } ); -$.ui.plugin.add( "draggable", "snap", { - start: function( event, ui, i ) { - var o = i.options; +/*! + * jQuery UI Effects Explode 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - i.snapElements = []; +//>>label: Explode Effect +//>>group: Effects +/* eslint-disable max-len */ +//>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/explode-effect/ +//>>demos: http://jqueryui.com/effect/ - $( o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap ) - .each( function() { - var $t = $( this ), - $o = $t.offset(); - if ( this !== i.element[ 0 ] ) { - i.snapElements.push( { - item: this, - width: $t.outerWidth(), height: $t.outerHeight(), - top: $o.top, left: $o.left - } ); - } - } ); - }, - drag: function( event, ui, inst ) { +var effectsEffectExplode = $.effects.define( "explode", "hide", function( options, done ) { - var ts, bs, ls, rs, l, r, t, b, i, first, - o = inst.options, - d = o.snapTolerance, - x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, - y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; + var i, j, left, top, mx, my, + rows = options.pieces ? Math.round( Math.sqrt( options.pieces ) ) : 3, + cells = rows, + element = $( this ), + mode = options.mode, + show = mode === "show", - for ( i = inst.snapElements.length - 1; i >= 0; i-- ) { + // Show and then visibility:hidden the element before calculating offset + offset = element.show().css( "visibility", "hidden" ).offset(), - l = inst.snapElements[ i ].left - inst.margins.left; - r = l + inst.snapElements[ i ].width; - t = inst.snapElements[ i ].top - inst.margins.top; - b = t + inst.snapElements[ i ].height; + // Width and height of a piece + width = Math.ceil( element.outerWidth() / cells ), + height = Math.ceil( element.outerHeight() / rows ), + pieces = []; - if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || - !$.contains( inst.snapElements[ i ].item.ownerDocument, - inst.snapElements[ i ].item ) ) { - if ( inst.snapElements[ i ].snapping ) { - if ( inst.options.snap.release ) { - inst.options.snap.release.call( - inst.element, - event, - $.extend( inst._uiHash(), { snapItem: inst.snapElements[ i ].item } ) - ); - } - } - inst.snapElements[ i ].snapping = false; - continue; - } + // Children animate complete: + function childComplete() { + pieces.push( this ); + if ( pieces.length === rows * cells ) { + animComplete(); + } + } - if ( o.snapMode !== "inner" ) { - ts = Math.abs( t - y2 ) <= d; - bs = Math.abs( b - y1 ) <= d; - ls = Math.abs( l - x2 ) <= d; - rs = Math.abs( r - x1 ) <= d; - if ( ts ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: t - inst.helperProportions.height, - left: 0 - } ).top; - } - if ( bs ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: b, - left: 0 - } ).top; - } - if ( ls ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: l - inst.helperProportions.width - } ).left; - } - if ( rs ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: r - } ).left; - } - } + // Clone the element for each row and cell. + for ( i = 0; i < rows; i++ ) { // ===> + top = offset.top + i * height; + my = i - ( rows - 1 ) / 2; - first = ( ts || bs || ls || rs ); - - if ( o.snapMode !== "outer" ) { - ts = Math.abs( t - y1 ) <= d; - bs = Math.abs( b - y2 ) <= d; - ls = Math.abs( l - x1 ) <= d; - rs = Math.abs( r - x2 ) <= d; - if ( ts ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: t, - left: 0 - } ).top; - } - if ( bs ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: b - inst.helperProportions.height, - left: 0 - } ).top; - } - if ( ls ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: l - } ).left; - } - if ( rs ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: r - inst.helperProportions.width - } ).left; - } - } + for ( j = 0; j < cells; j++ ) { // ||| + left = offset.left + j * width; + mx = j - ( cells - 1 ) / 2; - if ( !inst.snapElements[ i ].snapping && ( ts || bs || ls || rs || first ) ) { - if ( inst.options.snap.snap ) { - inst.options.snap.snap.call( - inst.element, - event, - $.extend( inst._uiHash(), { - snapItem: inst.snapElements[ i ].item - } ) ); - } - } - inst.snapElements[ i ].snapping = ( ts || bs || ls || rs || first ); + // Create a clone of the now hidden main element that will be absolute positioned + // within a wrapper div off the -left and -top equal to size of our pieces + element + .clone() + .appendTo( "body" ) + .wrap( "<div></div>" ) + .css( { + position: "absolute", + visibility: "visible", + left: -j * width, + top: -i * height + } ) + // Select the wrapper - make it overflow: hidden and absolute positioned based on + // where the original was located +left and +top equal to the size of pieces + .parent() + .addClass( "ui-effects-explode" ) + .css( { + position: "absolute", + overflow: "hidden", + width: width, + height: height, + left: left + ( show ? mx * width : 0 ), + top: top + ( show ? my * height : 0 ), + opacity: show ? 0 : 1 + } ) + .animate( { + left: left + ( show ? 0 : mx * width ), + top: top + ( show ? 0 : my * height ), + opacity: show ? 1 : 0 + }, options.duration || 500, options.easing, childComplete ); } + } + function animComplete() { + element.css( { + visibility: "visible" + } ); + $( pieces ).remove(); + done(); } } ); -$.ui.plugin.add( "draggable", "stack", { - start: function( event, ui, instance ) { - var min, - o = instance.options, - group = $.makeArray( $( o.stack ) ).sort( function( a, b ) { - return ( parseInt( $( a ).css( "zIndex" ), 10 ) || 0 ) - - ( parseInt( $( b ).css( "zIndex" ), 10 ) || 0 ); - } ); - if ( !group.length ) { - return; - } +/*! + * jQuery UI Effects Fade 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - min = parseInt( $( group[ 0 ] ).css( "zIndex" ), 10 ) || 0; - $( group ).each( function( i ) { - $( this ).css( "zIndex", min + i ); - } ); - this.css( "zIndex", ( min + group.length ) ); - } -} ); +//>>label: Fade Effect +//>>group: Effects +//>>description: Fades the element. +//>>docs: http://api.jqueryui.com/fade-effect/ +//>>demos: http://jqueryui.com/effect/ -$.ui.plugin.add( "draggable", "zIndex", { - start: function( event, ui, instance ) { - var t = $( ui.helper ), - o = instance.options; - if ( t.css( "zIndex" ) ) { - o._zIndex = t.css( "zIndex" ); - } - t.css( "zIndex", o.zIndex ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; +var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, done ) { + var show = options.mode === "show"; - if ( o._zIndex ) { - $( ui.helper ).css( "zIndex", o._zIndex ); - } - } + $( this ) + .css( "opacity", show ? 0 : 1 ) + .animate( { + opacity: show ? 1 : 0 + }, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); } ); -var widgetsDraggable = $.ui.draggable; - /*! - * jQuery UI Droppable 1.13.1 + * jQuery UI Effects Fold 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3271,482 +3389,408 @@ var widgetsDraggable = $.ui.draggable; * http://jquery.org/license */ -//>>label: Droppable -//>>group: Interactions -//>>description: Enables drop targets for draggable elements. -//>>docs: http://api.jqueryui.com/droppable/ -//>>demos: http://jqueryui.com/droppable/ - - -$.widget( "ui.droppable", { - version: "1.13.1", - widgetEventPrefix: "drop", - options: { - accept: "*", - addClasses: true, - greedy: false, - scope: "default", - tolerance: "intersect", +//>>label: Fold Effect +//>>group: Effects +//>>description: Folds an element first horizontally and then vertically. +//>>docs: http://api.jqueryui.com/fold-effect/ +//>>demos: http://jqueryui.com/effect/ - // Callbacks - activate: null, - deactivate: null, - drop: null, - out: null, - over: null - }, - _create: function() { - var proportions, - o = this.options, - accept = o.accept; +var effectsEffectFold = $.effects.define( "fold", "hide", function( options, done ) { - this.isover = false; - this.isout = true; + // Create element + var element = $( this ), + mode = options.mode, + show = mode === "show", + hide = mode === "hide", + size = options.size || 15, + percent = /([0-9]+)%/.exec( size ), + horizFirst = !!options.horizFirst, + ref = horizFirst ? [ "right", "bottom" ] : [ "bottom", "right" ], + duration = options.duration / 2, - this.accept = typeof accept === "function" ? accept : function( d ) { - return d.is( accept ); - }; + placeholder = $.effects.createPlaceholder( element ), - this.proportions = function( /* valueToWrite */ ) { - if ( arguments.length ) { + start = element.cssClip(), + animation1 = { clip: $.extend( {}, start ) }, + animation2 = { clip: $.extend( {}, start ) }, - // Store the droppable's proportions - proportions = arguments[ 0 ]; - } else { + distance = [ start[ ref[ 0 ] ], start[ ref[ 1 ] ] ], - // Retrieve or derive the droppable's proportions - return proportions ? - proportions : - proportions = { - width: this.element[ 0 ].offsetWidth, - height: this.element[ 0 ].offsetHeight - }; - } - }; + queuelen = element.queue().length; - this._addToManager( o.scope ); + if ( percent ) { + size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ]; + } + animation1.clip[ ref[ 0 ] ] = size; + animation2.clip[ ref[ 0 ] ] = size; + animation2.clip[ ref[ 1 ] ] = 0; - if ( o.addClasses ) { - this._addClass( "ui-droppable" ); + if ( show ) { + element.cssClip( animation2.clip ); + if ( placeholder ) { + placeholder.css( $.effects.clipToBox( animation2 ) ); } - }, + animation2.clip = start; + } - _addToManager: function( scope ) { + // Animate + element + .queue( function( next ) { + if ( placeholder ) { + placeholder + .animate( $.effects.clipToBox( animation1 ), duration, options.easing ) + .animate( $.effects.clipToBox( animation2 ), duration, options.easing ); + } - // Add the reference and positions to the manager - $.ui.ddmanager.droppables[ scope ] = $.ui.ddmanager.droppables[ scope ] || []; - $.ui.ddmanager.droppables[ scope ].push( this ); - }, + next(); + } ) + .animate( animation1, duration, options.easing ) + .animate( animation2, duration, options.easing ) + .queue( done ); - _splice: function( drop ) { - var i = 0; - for ( ; i < drop.length; i++ ) { - if ( drop[ i ] === this ) { - drop.splice( i, 1 ); - } - } - }, + $.effects.unshift( element, queuelen, 4 ); +} ); - _destroy: function() { - var drop = $.ui.ddmanager.droppables[ this.options.scope ]; - this._splice( drop ); - }, +/*! + * jQuery UI Effects Highlight 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _setOption: function( key, value ) { +//>>label: Highlight Effect +//>>group: Effects +//>>description: Highlights the background of an element in a defined color for a custom duration. +//>>docs: http://api.jqueryui.com/highlight-effect/ +//>>demos: http://jqueryui.com/effect/ - if ( key === "accept" ) { - this.accept = typeof value === "function" ? value : function( d ) { - return d.is( value ); - }; - } else if ( key === "scope" ) { - var drop = $.ui.ddmanager.droppables[ this.options.scope ]; - this._splice( drop ); - this._addToManager( value ); - } +var effectsEffectHighlight = $.effects.define( "highlight", "show", function( options, done ) { + var element = $( this ), + animation = { + backgroundColor: element.css( "backgroundColor" ) + }; - this._super( key, value ); - }, + if ( options.mode === "hide" ) { + animation.opacity = 0; + } - _activate: function( event ) { - var draggable = $.ui.ddmanager.current; + $.effects.saveStyle( element ); - this._addActiveClass(); - if ( draggable ) { - this._trigger( "activate", event, this.ui( draggable ) ); - } - }, + element + .css( { + backgroundImage: "none", + backgroundColor: options.color || "#ffff99" + } ) + .animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); - _deactivate: function( event ) { - var draggable = $.ui.ddmanager.current; - this._removeActiveClass(); - if ( draggable ) { - this._trigger( "deactivate", event, this.ui( draggable ) ); - } - }, +/*! + * jQuery UI Effects Size 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _over: function( event ) { +//>>label: Size Effect +//>>group: Effects +//>>description: Resize an element to a specified width and height. +//>>docs: http://api.jqueryui.com/size-effect/ +//>>demos: http://jqueryui.com/effect/ - var draggable = $.ui.ddmanager.current; - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return; - } +var effectsEffectSize = $.effects.define( "size", function( options, done ) { - if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || - draggable.element ) ) ) { - this._addHoverClass(); - this._trigger( "over", event, this.ui( draggable ) ); - } + // Create element + var baseline, factor, temp, + element = $( this ), - }, + // Copy for children + cProps = [ "fontSize" ], + vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ], + hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ], - _out: function( event ) { + // Set options + mode = options.mode, + restore = mode !== "effect", + scale = options.scale || "both", + origin = options.origin || [ "middle", "center" ], + position = element.css( "position" ), + pos = element.position(), + original = $.effects.scaledDimensions( element ), + from = options.from || original, + to = options.to || $.effects.scaledDimensions( element, 0 ); - var draggable = $.ui.ddmanager.current; + $.effects.createPlaceholder( element ); - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return; - } + if ( mode === "show" ) { + temp = from; + from = to; + to = temp; + } - if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || - draggable.element ) ) ) { - this._removeHoverClass(); - this._trigger( "out", event, this.ui( draggable ) ); + // Set scaling factor + factor = { + from: { + y: from.height / original.height, + x: from.width / original.width + }, + to: { + y: to.height / original.height, + x: to.width / original.width } + }; - }, - - _drop: function( event, custom ) { - - var draggable = custom || $.ui.ddmanager.current, - childrenIntersection = false; + // Scale the css box + if ( scale === "box" || scale === "both" ) { - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return false; + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + from = $.effects.setTransition( element, vProps, factor.from.y, from ); + to = $.effects.setTransition( element, vProps, factor.to.y, to ); } - this.element - .find( ":data(ui-droppable)" ) - .not( ".ui-draggable-dragging" ) - .each( function() { - var inst = $( this ).droppable( "instance" ); - if ( - inst.options.greedy && - !inst.options.disabled && - inst.options.scope === draggable.options.scope && - inst.accept.call( - inst.element[ 0 ], ( draggable.currentItem || draggable.element ) - ) && - $.ui.intersect( - draggable, - $.extend( inst, { offset: inst.element.offset() } ), - inst.options.tolerance, event - ) - ) { - childrenIntersection = true; - return false; - } - } ); - if ( childrenIntersection ) { - return false; + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + from = $.effects.setTransition( element, hProps, factor.from.x, from ); + to = $.effects.setTransition( element, hProps, factor.to.x, to ); } + } - if ( this.accept.call( this.element[ 0 ], - ( draggable.currentItem || draggable.element ) ) ) { - this._removeActiveClass(); - this._removeHoverClass(); + // Scale the content + if ( scale === "content" || scale === "both" ) { - this._trigger( "drop", event, this.ui( draggable ) ); - return this.element; + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + from = $.effects.setTransition( element, cProps, factor.from.y, from ); + to = $.effects.setTransition( element, cProps, factor.to.y, to ); } + } - return false; - - }, + // Adjust the position properties based on the provided origin points + if ( origin ) { + baseline = $.effects.getBaseline( origin, original ); + from.top = ( original.outerHeight - from.outerHeight ) * baseline.y + pos.top; + from.left = ( original.outerWidth - from.outerWidth ) * baseline.x + pos.left; + to.top = ( original.outerHeight - to.outerHeight ) * baseline.y + pos.top; + to.left = ( original.outerWidth - to.outerWidth ) * baseline.x + pos.left; + } + delete from.outerHeight; + delete from.outerWidth; + element.css( from ); - ui: function( c ) { - return { - draggable: ( c.currentItem || c.element ), - helper: c.helper, - position: c.position, - offset: c.positionAbs - }; - }, + // Animate the children if desired + if ( scale === "content" || scale === "both" ) { - // Extension points just to make backcompat sane and avoid duplicating logic - // TODO: Remove in 1.14 along with call to it below - _addHoverClass: function() { - this._addClass( "ui-droppable-hover" ); - }, + vProps = vProps.concat( [ "marginTop", "marginBottom" ] ).concat( cProps ); + hProps = hProps.concat( [ "marginLeft", "marginRight" ] ); - _removeHoverClass: function() { - this._removeClass( "ui-droppable-hover" ); - }, + // Only animate children with width attributes specified + // TODO: is this right? should we include anything with css width specified as well + element.find( "*[width]" ).each( function() { + var child = $( this ), + childOriginal = $.effects.scaledDimensions( child ), + childFrom = { + height: childOriginal.height * factor.from.y, + width: childOriginal.width * factor.from.x, + outerHeight: childOriginal.outerHeight * factor.from.y, + outerWidth: childOriginal.outerWidth * factor.from.x + }, + childTo = { + height: childOriginal.height * factor.to.y, + width: childOriginal.width * factor.to.x, + outerHeight: childOriginal.height * factor.to.y, + outerWidth: childOriginal.width * factor.to.x + }; - _addActiveClass: function() { - this._addClass( "ui-droppable-active" ); - }, + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + childFrom = $.effects.setTransition( child, vProps, factor.from.y, childFrom ); + childTo = $.effects.setTransition( child, vProps, factor.to.y, childTo ); + } - _removeActiveClass: function() { - this._removeClass( "ui-droppable-active" ); - } -} ); + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + childFrom = $.effects.setTransition( child, hProps, factor.from.x, childFrom ); + childTo = $.effects.setTransition( child, hProps, factor.to.x, childTo ); + } -$.ui.intersect = ( function() { - function isOverAxis( x, reference, size ) { - return ( x >= reference ) && ( x < ( reference + size ) ); - } - - return function( draggable, droppable, toleranceMode, event ) { - - if ( !droppable.offset ) { - return false; - } - - var x1 = ( draggable.positionAbs || - draggable.position.absolute ).left + draggable.margins.left, - y1 = ( draggable.positionAbs || - draggable.position.absolute ).top + draggable.margins.top, - x2 = x1 + draggable.helperProportions.width, - y2 = y1 + draggable.helperProportions.height, - l = droppable.offset.left, - t = droppable.offset.top, - r = l + droppable.proportions().width, - b = t + droppable.proportions().height; - - switch ( toleranceMode ) { - case "fit": - return ( l <= x1 && x2 <= r && t <= y1 && y2 <= b ); - case "intersect": - return ( l < x1 + ( draggable.helperProportions.width / 2 ) && // Right Half - x2 - ( draggable.helperProportions.width / 2 ) < r && // Left Half - t < y1 + ( draggable.helperProportions.height / 2 ) && // Bottom Half - y2 - ( draggable.helperProportions.height / 2 ) < b ); // Top Half - case "pointer": - return isOverAxis( event.pageY, t, droppable.proportions().height ) && - isOverAxis( event.pageX, l, droppable.proportions().width ); - case "touch": - return ( - ( y1 >= t && y1 <= b ) || // Top edge touching - ( y2 >= t && y2 <= b ) || // Bottom edge touching - ( y1 < t && y2 > b ) // Surrounded vertically - ) && ( - ( x1 >= l && x1 <= r ) || // Left edge touching - ( x2 >= l && x2 <= r ) || // Right edge touching - ( x1 < l && x2 > r ) // Surrounded horizontally - ); - default: - return false; - } - }; -} )(); + if ( restore ) { + $.effects.saveStyle( child ); + } -/* - This manager tracks offsets of draggables and droppables -*/ -$.ui.ddmanager = { - current: null, - droppables: { "default": [] }, - prepareOffsets: function( t, event ) { + // Animate children + child.css( childFrom ); + child.animate( childTo, options.duration, options.easing, function() { - var i, j, - m = $.ui.ddmanager.droppables[ t.options.scope ] || [], - type = event ? event.type : null, // workaround for #2317 - list = ( t.currentItem || t.element ).find( ":data(ui-droppable)" ).addBack(); + // Restore children + if ( restore ) { + $.effects.restoreStyle( child ); + } + } ); + } ); + } - droppablesLoop: for ( i = 0; i < m.length; i++ ) { + // Animate + element.animate( to, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: function() { - // No disabled and non-accepted - if ( m[ i ].options.disabled || ( t && !m[ i ].accept.call( m[ i ].element[ 0 ], - ( t.currentItem || t.element ) ) ) ) { - continue; - } + var offset = element.offset(); - // Filter out elements in the current dragged item - for ( j = 0; j < list.length; j++ ) { - if ( list[ j ] === m[ i ].element[ 0 ] ) { - m[ i ].proportions().height = 0; - continue droppablesLoop; - } + if ( to.opacity === 0 ) { + element.css( "opacity", from.opacity ); } - m[ i ].visible = m[ i ].element.css( "display" ) !== "none"; - if ( !m[ i ].visible ) { - continue; - } + if ( !restore ) { + element + .css( "position", position === "static" ? "relative" : position ) + .offset( offset ); - // Activate the droppable if used directly from draggables - if ( type === "mousedown" ) { - m[ i ]._activate.call( m[ i ], event ); + // Need to save style here so that automatic style restoration + // doesn't restore to the original styles from before the animation. + $.effects.saveStyle( element ); } - m[ i ].offset = m[ i ].element.offset(); - m[ i ].proportions( { - width: m[ i ].element[ 0 ].offsetWidth, - height: m[ i ].element[ 0 ].offsetHeight - } ); - + done(); } + } ); - }, - drop: function( draggable, event ) { +} ); - var dropped = false; - // Create a copy of the droppables in case the list changes during the drop (#9116) - $.each( ( $.ui.ddmanager.droppables[ draggable.options.scope ] || [] ).slice(), function() { +/*! + * jQuery UI Effects Scale 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( !this.options ) { - return; - } - if ( !this.options.disabled && this.visible && - $.ui.intersect( draggable, this, this.options.tolerance, event ) ) { - dropped = this._drop.call( this, event ) || dropped; - } +//>>label: Scale Effect +//>>group: Effects +//>>description: Grows or shrinks an element and its content. +//>>docs: http://api.jqueryui.com/scale-effect/ +//>>demos: http://jqueryui.com/effect/ - if ( !this.options.disabled && this.visible && this.accept.call( this.element[ 0 ], - ( draggable.currentItem || draggable.element ) ) ) { - this.isout = true; - this.isover = false; - this._deactivate.call( this, event ); - } - } ); - return dropped; +var effectsEffectScale = $.effects.define( "scale", function( options, done ) { - }, - dragStart: function( draggable, event ) { + // Create element + var el = $( this ), + mode = options.mode, + percent = parseInt( options.percent, 10 ) || + ( parseInt( options.percent, 10 ) === 0 ? 0 : ( mode !== "effect" ? 0 : 100 ) ), - // Listen for scrolling so that if the dragging causes scrolling the position of the - // droppables can be recalculated (see #5003) - draggable.element.parentsUntil( "body" ).on( "scroll.droppable", function() { - if ( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } ); - }, - drag: function( draggable, event ) { + newOptions = $.extend( true, { + from: $.effects.scaledDimensions( el ), + to: $.effects.scaledDimensions( el, percent, options.direction || "both" ), + origin: options.origin || [ "middle", "center" ] + }, options ); - // If you have a highly dynamic page, you might try this option. It renders positions - // every time you move the mouse. - if ( draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } + // Fade option to support puff + if ( options.fade ) { + newOptions.from.opacity = 1; + newOptions.to.opacity = 0; + } - // Run through all droppables and check their positions based on specific tolerance options - $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { + $.effects.effect.size.call( this, newOptions, done ); +} ); - if ( this.options.disabled || this.greedyChild || !this.visible ) { - return; - } - var parentInstance, scope, parent, - intersects = $.ui.intersect( draggable, this, this.options.tolerance, event ), - c = !intersects && this.isover ? - "isout" : - ( intersects && !this.isover ? "isover" : null ); - if ( !c ) { - return; - } +/*! + * jQuery UI Effects Puff 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( this.options.greedy ) { +//>>label: Puff Effect +//>>group: Effects +//>>description: Creates a puff effect by scaling the element up and hiding it at the same time. +//>>docs: http://api.jqueryui.com/puff-effect/ +//>>demos: http://jqueryui.com/effect/ - // find droppable parents with same scope - scope = this.options.scope; - parent = this.element.parents( ":data(ui-droppable)" ).filter( function() { - return $( this ).droppable( "instance" ).options.scope === scope; - } ); - if ( parent.length ) { - parentInstance = $( parent[ 0 ] ).droppable( "instance" ); - parentInstance.greedyChild = ( c === "isover" ); - } - } +var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, done ) { + var newOptions = $.extend( true, {}, options, { + fade: true, + percent: parseInt( options.percent, 10 ) || 150 + } ); - // We just moved into a greedy child - if ( parentInstance && c === "isover" ) { - parentInstance.isover = false; - parentInstance.isout = true; - parentInstance._out.call( parentInstance, event ); - } + $.effects.effect.scale.call( this, newOptions, done ); +} ); - this[ c ] = true; - this[ c === "isout" ? "isover" : "isout" ] = false; - this[ c === "isover" ? "_over" : "_out" ].call( this, event ); - // We just moved out of a greedy child - if ( parentInstance && c === "isout" ) { - parentInstance.isout = false; - parentInstance.isover = true; - parentInstance._over.call( parentInstance, event ); - } - } ); +/*! + * jQuery UI Effects Pulsate 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - }, - dragStop: function( draggable, event ) { - draggable.element.parentsUntil( "body" ).off( "scroll.droppable" ); +//>>label: Pulsate Effect +//>>group: Effects +//>>description: Pulsates an element n times by changing the opacity to zero and back. +//>>docs: http://api.jqueryui.com/pulsate-effect/ +//>>demos: http://jqueryui.com/effect/ - // Call prepareOffsets one final time since IE does not fire return scroll events when - // overflow was caused by drag (see #5003) - if ( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } + +var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( options, done ) { + var element = $( this ), + mode = options.mode, + show = mode === "show", + hide = mode === "hide", + showhide = show || hide, + + // Showing or hiding leaves off the "last" animation + anims = ( ( options.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ), + duration = options.duration / anims, + animateTo = 0, + i = 1, + queuelen = element.queue().length; + + if ( show || !element.is( ":visible" ) ) { + element.css( "opacity", 0 ).show(); + animateTo = 1; } -}; -// DEPRECATED -// TODO: switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { + // Anims - 1 opacity "toggles" + for ( ; i < anims; i++ ) { + element.animate( { opacity: animateTo }, duration, options.easing ); + animateTo = 1 - animateTo; + } - // Backcompat for activeClass and hoverClass options - $.widget( "ui.droppable", $.ui.droppable, { - options: { - hoverClass: false, - activeClass: false - }, - _addActiveClass: function() { - this._super(); - if ( this.options.activeClass ) { - this.element.addClass( this.options.activeClass ); - } - }, - _removeActiveClass: function() { - this._super(); - if ( this.options.activeClass ) { - this.element.removeClass( this.options.activeClass ); - } - }, - _addHoverClass: function() { - this._super(); - if ( this.options.hoverClass ) { - this.element.addClass( this.options.hoverClass ); - } - }, - _removeHoverClass: function() { - this._super(); - if ( this.options.hoverClass ) { - this.element.removeClass( this.options.hoverClass ); - } - } - } ); -} + element.animate( { opacity: animateTo }, duration, options.easing ); -var widgetsDroppable = $.ui.droppable; + element.queue( done ); + + $.effects.unshift( element, queuelen, anims + 1 ); +} ); /*! - * jQuery UI Resizable 1.13.1 + * jQuery UI Effects Shake 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3754,1197 +3798,1181 @@ var widgetsDroppable = $.ui.droppable; * http://jquery.org/license */ -//>>label: Resizable -//>>group: Interactions -//>>description: Enables resize functionality for any element. -//>>docs: http://api.jqueryui.com/resizable/ -//>>demos: http://jqueryui.com/resizable/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/resizable.css -//>>css.theme: ../../themes/base/theme.css +//>>label: Shake Effect +//>>group: Effects +//>>description: Shakes an element horizontally or vertically n times. +//>>docs: http://api.jqueryui.com/shake-effect/ +//>>demos: http://jqueryui.com/effect/ -$.widget( "ui.resizable", $.ui.mouse, { - version: "1.13.1", - widgetEventPrefix: "resize", - options: { - alsoResize: false, - animate: false, - animateDuration: "slow", - animateEasing: "swing", - aspectRatio: false, - autoHide: false, - classes: { - "ui-resizable-se": "ui-icon ui-icon-gripsmall-diagonal-se" - }, - containment: false, - ghost: false, - grid: false, - handles: "e,s,se", - helper: false, - maxHeight: null, - maxWidth: null, - minHeight: 10, - minWidth: 10, +var effectsEffectShake = $.effects.define( "shake", function( options, done ) { - // See #7960 - zIndex: 90, + var i = 1, + element = $( this ), + direction = options.direction || "left", + distance = options.distance || 20, + times = options.times || 3, + anims = times * 2 + 1, + speed = Math.round( options.duration / anims ), + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + positiveMotion = ( direction === "up" || direction === "left" ), + animation = {}, + animation1 = {}, + animation2 = {}, - // Callbacks - resize: null, - start: null, - stop: null - }, + queuelen = element.queue().length; - _num: function( value ) { - return parseFloat( value ) || 0; - }, + $.effects.createPlaceholder( element ); - _isNumber: function( value ) { - return !isNaN( parseFloat( value ) ); - }, + // Animation + animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance; + animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2; + animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2; - _hasScroll: function( el, a ) { + // Animate + element.animate( animation, speed, options.easing ); - if ( $( el ).css( "overflow" ) === "hidden" ) { - return false; - } + // Shakes + for ( ; i < times; i++ ) { + element + .animate( animation1, speed, options.easing ) + .animate( animation2, speed, options.easing ); + } - var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", - has = false; + element + .animate( animation1, speed, options.easing ) + .animate( animation, speed / 2, options.easing ) + .queue( done ); - if ( el[ scroll ] > 0 ) { - return true; - } + $.effects.unshift( element, queuelen, anims + 1 ); +} ); - // TODO: determine which cases actually cause this to happen - // if the element doesn't have the scroll set, see if it's possible to - // set the scroll - try { - el[ scroll ] = 1; - has = ( el[ scroll ] > 0 ); - el[ scroll ] = 0; - } catch ( e ) { - // `el` might be a string, then setting `scroll` will throw - // an error in strict mode; ignore it. - } - return has; - }, +/*! + * jQuery UI Effects Slide 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _create: function() { +//>>label: Slide Effect +//>>group: Effects +//>>description: Slides an element in and out of the viewport. +//>>docs: http://api.jqueryui.com/slide-effect/ +//>>demos: http://jqueryui.com/effect/ - var margins, - o = this.options, - that = this; - this._addClass( "ui-resizable" ); - $.extend( this, { - _aspectRatio: !!( o.aspectRatio ), - aspectRatio: o.aspectRatio, - originalElement: this.element, - _proportionallyResizeElements: [], - _helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null - } ); +var effectsEffectSlide = $.effects.define( "slide", "show", function( options, done ) { + var startClip, startRef, + element = $( this ), + map = { + up: [ "bottom", "top" ], + down: [ "top", "bottom" ], + left: [ "right", "left" ], + right: [ "left", "right" ] + }, + mode = options.mode, + direction = options.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + positiveMotion = ( direction === "up" || direction === "left" ), + distance = options.distance || + element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ), + animation = {}; - // Wrap the element if it cannot hold child nodes - if ( this.element[ 0 ].nodeName.match( /^(canvas|textarea|input|select|button|img)$/i ) ) { + $.effects.createPlaceholder( element ); - this.element.wrap( - $( "<div class='ui-wrapper'></div>" ).css( { - overflow: "hidden", - position: this.element.css( "position" ), - width: this.element.outerWidth(), - height: this.element.outerHeight(), - top: this.element.css( "top" ), - left: this.element.css( "left" ) - } ) - ); + startClip = element.cssClip(); + startRef = element.position()[ ref ]; - this.element = this.element.parent().data( - "ui-resizable", this.element.resizable( "instance" ) - ); + // Define hide animation + animation[ ref ] = ( positiveMotion ? -1 : 1 ) * distance + startRef; + animation.clip = element.cssClip(); + animation.clip[ map[ direction ][ 1 ] ] = animation.clip[ map[ direction ][ 0 ] ]; - this.elementIsWrapper = true; + // Reverse the animation if we're showing + if ( mode === "show" ) { + element.cssClip( animation.clip ); + element.css( ref, animation[ ref ] ); + animation.clip = startClip; + animation[ ref ] = startRef; + } - margins = { - marginTop: this.originalElement.css( "marginTop" ), - marginRight: this.originalElement.css( "marginRight" ), - marginBottom: this.originalElement.css( "marginBottom" ), - marginLeft: this.originalElement.css( "marginLeft" ) - }; + // Actually animate + element.animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); - this.element.css( margins ); - this.originalElement.css( "margin", 0 ); - - // support: Safari - // Prevent Safari textarea resize - this.originalResizeStyle = this.originalElement.css( "resize" ); - this.originalElement.css( "resize", "none" ); - this._proportionallyResizeElements.push( this.originalElement.css( { - position: "static", - zoom: 1, - display: "block" - } ) ); +/*! + * jQuery UI Effects Transfer 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - // Support: IE9 - // avoid IE jump (hard set the margin) - this.originalElement.css( margins ); +//>>label: Transfer Effect +//>>group: Effects +//>>description: Displays a transfer effect from one element to another. +//>>docs: http://api.jqueryui.com/transfer-effect/ +//>>demos: http://jqueryui.com/effect/ - this._proportionallyResize(); - } - this._setupHandles(); +var effect; +if ( $.uiBackCompat !== false ) { + effect = $.effects.define( "transfer", function( options, done ) { + $( this ).transfer( options, done ); + } ); +} +var effectsEffectTransfer = effect; - if ( o.autoHide ) { - $( this.element ) - .on( "mouseenter", function() { - if ( o.disabled ) { - return; - } - that._removeClass( "ui-resizable-autohide" ); - that._handles.show(); - } ) - .on( "mouseleave", function() { - if ( o.disabled ) { - return; - } - if ( !that.resizing ) { - that._addClass( "ui-resizable-autohide" ); - that._handles.hide(); - } - } ); - } - this._mouseInit(); - }, +/*! + * jQuery UI Focusable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _destroy: function() { +//>>label: :focusable Selector +//>>group: Core +//>>description: Selects elements which can be focused. +//>>docs: http://api.jqueryui.com/focusable-selector/ - this._mouseDestroy(); - this._addedHandles.remove(); - var wrapper, - _destroy = function( exp ) { - $( exp ) - .removeData( "resizable" ) - .removeData( "ui-resizable" ) - .off( ".resizable" ); - }; +// Selectors +$.ui.focusable = function( element, hasTabindex ) { + var map, mapName, img, focusableIfVisible, fieldset, + nodeName = element.nodeName.toLowerCase(); - // TODO: Unwrap at same DOM position - if ( this.elementIsWrapper ) { - _destroy( this.element ); - wrapper = this.element; - this.originalElement.css( { - position: wrapper.css( "position" ), - width: wrapper.outerWidth(), - height: wrapper.outerHeight(), - top: wrapper.css( "top" ), - left: wrapper.css( "left" ) - } ).insertAfter( wrapper ); - wrapper.remove(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; } + img = $( "img[usemap='#" + mapName + "']" ); + return img.length > 0 && img.is( ":visible" ); + } - this.originalElement.css( "resize", this.originalResizeStyle ); - _destroy( this.originalElement ); - - return this; - }, + if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) { + focusableIfVisible = !element.disabled; - _setOption: function( key, value ) { - this._super( key, value ); + if ( focusableIfVisible ) { - switch ( key ) { - case "handles": - this._removeHandles(); - this._setupHandles(); - break; - case "aspectRatio": - this._aspectRatio = !!value; - break; - default: - break; + // Form controls within a disabled fieldset are disabled. + // However, controls within the fieldset's legend do not get disabled. + // Since controls generally aren't placed inside legends, we skip + // this portion of the check. + fieldset = $( element ).closest( "fieldset" )[ 0 ]; + if ( fieldset ) { + focusableIfVisible = !fieldset.disabled; + } } - }, - - _setupHandles: function() { - var o = this.options, handle, i, n, hname, axis, that = this; - this.handles = o.handles || - ( !$( ".ui-resizable-handle", this.element ).length ? - "e,s,se" : { - n: ".ui-resizable-n", - e: ".ui-resizable-e", - s: ".ui-resizable-s", - w: ".ui-resizable-w", - se: ".ui-resizable-se", - sw: ".ui-resizable-sw", - ne: ".ui-resizable-ne", - nw: ".ui-resizable-nw" - } ); - - this._handles = $(); - this._addedHandles = $(); - if ( this.handles.constructor === String ) { + } else if ( "a" === nodeName ) { + focusableIfVisible = element.href || hasTabindex; + } else { + focusableIfVisible = hasTabindex; + } - if ( this.handles === "all" ) { - this.handles = "n,e,s,w,se,sw,ne,nw"; - } + return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); +}; - n = this.handles.split( "," ); - this.handles = {}; +// Support: IE 8 only +// IE 8 doesn't resolve inherit to visible/hidden for computed values +function visible( element ) { + var visibility = element.css( "visibility" ); + while ( visibility === "inherit" ) { + element = element.parent(); + visibility = element.css( "visibility" ); + } + return visibility === "visible"; +} - for ( i = 0; i < n.length; i++ ) { +$.extend( $.expr.pseudos, { + focusable: function( element ) { + return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); + } +} ); - handle = String.prototype.trim.call( n[ i ] ); - hname = "ui-resizable-" + handle; - axis = $( "<div>" ); - this._addClass( axis, "ui-resizable-handle " + hname ); +var focusable = $.ui.focusable; - axis.css( { zIndex: o.zIndex } ); - this.handles[ handle ] = ".ui-resizable-" + handle; - if ( !this.element.children( this.handles[ handle ] ).length ) { - this.element.append( axis ); - this._addedHandles = this._addedHandles.add( axis ); - } - } - } +// Support: IE8 Only +// IE8 does not support the form attribute and when it is supplied. It overwrites the form prop +// with a string, so we need to find the proper form. +var form = $.fn._form = function() { + return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); +}; - this._renderAxis = function( target ) { - var i, axis, padPos, padWrapper; +/*! + * jQuery UI Form Reset Mixin 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - target = target || this.element; +//>>label: Form Reset Mixin +//>>group: Core +//>>description: Refresh input widgets when their form is reset +//>>docs: http://api.jqueryui.com/form-reset-mixin/ - for ( i in this.handles ) { - if ( this.handles[ i ].constructor === String ) { - this.handles[ i ] = this.element.children( this.handles[ i ] ).first().show(); - } else if ( this.handles[ i ].jquery || this.handles[ i ].nodeType ) { - this.handles[ i ] = $( this.handles[ i ] ); - this._on( this.handles[ i ], { "mousedown": that._mouseDown } ); - } +var formResetMixin = $.ui.formResetMixin = { + _formResetHandler: function() { + var form = $( this ); - if ( this.elementIsWrapper && - this.originalElement[ 0 ] - .nodeName - .match( /^(textarea|input|select|button)$/i ) ) { - axis = $( this.handles[ i ], this.element ); + // Wait for the form reset to actually happen before refreshing + setTimeout( function() { + var instances = form.data( "ui-form-reset-instances" ); + $.each( instances, function() { + this.refresh(); + } ); + } ); + }, - padWrapper = /sw|ne|nw|se|n|s/.test( i ) ? - axis.outerHeight() : - axis.outerWidth(); + _bindFormResetHandler: function() { + this.form = this.element._form(); + if ( !this.form.length ) { + return; + } - padPos = [ "padding", - /ne|nw|n/.test( i ) ? "Top" : - /se|sw|s/.test( i ) ? "Bottom" : - /^e$/.test( i ) ? "Right" : "Left" ].join( "" ); + var instances = this.form.data( "ui-form-reset-instances" ) || []; + if ( !instances.length ) { - target.css( padPos, padWrapper ); + // We don't use _on() here because we use a single event handler per form + this.form.on( "reset.ui-form-reset", this._formResetHandler ); + } + instances.push( this ); + this.form.data( "ui-form-reset-instances", instances ); + }, - this._proportionallyResize(); - } + _unbindFormResetHandler: function() { + if ( !this.form.length ) { + return; + } - this._handles = this._handles.add( this.handles[ i ] ); - } - }; + var instances = this.form.data( "ui-form-reset-instances" ); + instances.splice( $.inArray( this, instances ), 1 ); + if ( instances.length ) { + this.form.data( "ui-form-reset-instances", instances ); + } else { + this.form + .removeData( "ui-form-reset-instances" ) + .off( "reset.ui-form-reset" ); + } + } +}; - // TODO: make renderAxis a prototype function - this._renderAxis( this.element ); - this._handles = this._handles.add( this.element.find( ".ui-resizable-handle" ) ); - this._handles.disableSelection(); +/*! + * jQuery UI Support for jQuery core 1.8.x and newer 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + */ - this._handles.on( "mouseover", function() { - if ( !that.resizing ) { - if ( this.className ) { - axis = this.className.match( /ui-resizable-(se|sw|ne|nw|n|e|s|w)/i ); - } - that.axis = axis && axis[ 1 ] ? axis[ 1 ] : "se"; - } - } ); +//>>label: jQuery 1.8+ Support +//>>group: Core +//>>description: Support version 1.8.x and newer of jQuery core - if ( o.autoHide ) { - this._handles.hide(); - this._addClass( "ui-resizable-autohide" ); - } - }, - _removeHandles: function() { - this._addedHandles.remove(); - }, +// Support: jQuery 1.9.x or older +// $.expr[ ":" ] is deprecated. +if ( !$.expr.pseudos ) { + $.expr.pseudos = $.expr[ ":" ]; +} - _mouseCapture: function( event ) { - var i, handle, - capture = false; +// Support: jQuery 1.11.x or older +// $.unique has been renamed to $.uniqueSort +if ( !$.uniqueSort ) { + $.uniqueSort = $.unique; +} - for ( i in this.handles ) { - handle = $( this.handles[ i ] )[ 0 ]; - if ( handle === event.target || $.contains( handle, event.target ) ) { - capture = true; - } - } +// Support: jQuery 2.2.x or older. +// This method has been defined in jQuery 3.0.0. +// Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js +if ( !$.escapeSelector ) { - return !this.options.disabled && capture; - }, + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - _mouseStart: function( event ) { + var fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { - var curleft, curtop, cursor, - o = this.options, - el = this.element; + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } - this.resizing = true; + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } - this._renderProxy(); + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }; - curleft = this._num( this.helper.css( "left" ) ); - curtop = this._num( this.helper.css( "top" ) ); + $.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); + }; +} - if ( o.containment ) { - curleft += $( o.containment ).scrollLeft() || 0; - curtop += $( o.containment ).scrollTop() || 0; +// Support: jQuery 3.4.x or older +// These methods have been defined in jQuery 3.5.0. +if ( !$.fn.even || !$.fn.odd ) { + $.fn.extend( { + even: function() { + return this.filter( function( i ) { + return i % 2 === 0; + } ); + }, + odd: function() { + return this.filter( function( i ) { + return i % 2 === 1; + } ); } + } ); +} - this.offset = this.helper.offset(); - this.position = { left: curleft, top: curtop }; - - this.size = this._helper ? { - width: this.helper.width(), - height: this.helper.height() - } : { - width: el.width(), - height: el.height() - }; - - this.originalSize = this._helper ? { - width: el.outerWidth(), - height: el.outerHeight() - } : { - width: el.width(), - height: el.height() - }; +; +/*! + * jQuery UI Keycode 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - this.sizeDiff = { - width: el.outerWidth() - el.width(), - height: el.outerHeight() - el.height() - }; +//>>label: Keycode +//>>group: Core +//>>description: Provide keycodes as keynames +//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ - this.originalPosition = { left: curleft, top: curtop }; - this.originalMousePosition = { left: event.pageX, top: event.pageY }; - this.aspectRatio = ( typeof o.aspectRatio === "number" ) ? - o.aspectRatio : - ( ( this.originalSize.width / this.originalSize.height ) || 1 ); +var keycode = $.ui.keyCode = { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 +}; - cursor = $( ".ui-resizable-" + this.axis ).css( "cursor" ); - $( "body" ).css( "cursor", cursor === "auto" ? this.axis + "-resize" : cursor ); - this._addClass( "ui-resizable-resizing" ); - this._propagate( "start", event ); - return true; - }, +/*! + * jQuery UI Labels 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _mouseDrag: function( event ) { +//>>label: labels +//>>group: Core +//>>description: Find all the labels associated with a given input +//>>docs: http://api.jqueryui.com/labels/ - var data, props, - smp = this.originalMousePosition, - a = this.axis, - dx = ( event.pageX - smp.left ) || 0, - dy = ( event.pageY - smp.top ) || 0, - trigger = this._change[ a ]; - this._updatePrevProperties(); +var labels = $.fn.labels = function() { + var ancestor, selector, id, labels, ancestors; - if ( !trigger ) { - return false; - } + if ( !this.length ) { + return this.pushStack( [] ); + } - data = trigger.apply( this, [ event, dx, dy ] ); + // Check control.labels first + if ( this[ 0 ].labels && this[ 0 ].labels.length ) { + return this.pushStack( this[ 0 ].labels ); + } - this._updateVirtualBoundaries( event.shiftKey ); - if ( this._aspectRatio || event.shiftKey ) { - data = this._updateRatio( data, event ); - } + // Support: IE <= 11, FF <= 37, Android <= 2.3 only + // Above browsers do not support control.labels. Everything below is to support them + // as well as document fragments. control.labels does not work on document fragments + labels = this.eq( 0 ).parents( "label" ); - data = this._respectSize( data, event ); + // Look for the label based on the id + id = this.attr( "id" ); + if ( id ) { - this._updateCache( data ); + // We don't search against the document in case the element + // is disconnected from the DOM + ancestor = this.eq( 0 ).parents().last(); - this._propagate( "resize", event ); + // Get a full set of top level ancestors + ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); - props = this._applyChanges(); + // Create a selector for the label based on the id + selector = "label[for='" + $.escapeSelector( id ) + "']"; - if ( !this._helper && this._proportionallyResizeElements.length ) { - this._proportionallyResize(); - } + labels = labels.add( ancestors.find( selector ).addBack( selector ) ); - if ( !$.isEmptyObject( props ) ) { - this._updatePrevProperties(); - this._trigger( "resize", event, this.ui() ); - this._applyChanges(); - } + } - return false; - }, + // Return whatever we have found for labels + return this.pushStack( labels ); +}; - _mouseStop: function( event ) { - this.resizing = false; - var pr, ista, soffseth, soffsetw, s, left, top, - o = this.options, that = this; - - if ( this._helper ) { +/*! + * jQuery UI Scroll Parent 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - pr = this._proportionallyResizeElements; - ista = pr.length && ( /textarea/i ).test( pr[ 0 ].nodeName ); - soffseth = ista && this._hasScroll( pr[ 0 ], "left" ) ? 0 : that.sizeDiff.height; - soffsetw = ista ? 0 : that.sizeDiff.width; +//>>label: scrollParent +//>>group: Core +//>>description: Get the closest ancestor element that is scrollable. +//>>docs: http://api.jqueryui.com/scrollParent/ - s = { - width: ( that.helper.width() - soffsetw ), - height: ( that.helper.height() - soffseth ) - }; - left = ( parseFloat( that.element.css( "left" ) ) + - ( that.position.left - that.originalPosition.left ) ) || null; - top = ( parseFloat( that.element.css( "top" ) ) + - ( that.position.top - that.originalPosition.top ) ) || null; - if ( !o.animate ) { - this.element.css( $.extend( s, { top: top, left: left } ) ); +var scrollParent = $.fn.scrollParent = function( includeHidden ) { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; } + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + + parent.css( "overflow-x" ) ); + } ).eq( 0 ); - that.helper.height( that.size.height ); - that.helper.width( that.size.width ); + return position === "fixed" || !scrollParent.length ? + $( this[ 0 ].ownerDocument || document ) : + scrollParent; +}; - if ( this._helper && !o.animate ) { - this._proportionallyResize(); - } - } - $( "body" ).css( "cursor", "auto" ); +/*! + * jQuery UI Tabbable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - this._removeClass( "ui-resizable-resizing" ); +//>>label: :tabbable Selector +//>>group: Core +//>>description: Selects elements which can be tabbed to. +//>>docs: http://api.jqueryui.com/tabbable-selector/ - this._propagate( "stop", event ); - if ( this._helper ) { - this.helper.remove(); - } +var tabbable = $.extend( $.expr.pseudos, { + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + hasTabindex = tabIndex != null; + return ( !hasTabindex || tabIndex >= 0 ) && $.ui.focusable( element, hasTabindex ); + } +} ); - return false; - }, +/*! + * jQuery UI Unique ID 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _updatePrevProperties: function() { - this.prevPosition = { - top: this.position.top, - left: this.position.left - }; - this.prevSize = { - width: this.size.width, - height: this.size.height - }; - }, +//>>label: uniqueId +//>>group: Core +//>>description: Functions to generate and remove uniqueId's +//>>docs: http://api.jqueryui.com/uniqueId/ - _applyChanges: function() { - var props = {}; - if ( this.position.top !== this.prevPosition.top ) { - props.top = this.position.top + "px"; - } - if ( this.position.left !== this.prevPosition.left ) { - props.left = this.position.left + "px"; - } - if ( this.size.width !== this.prevSize.width ) { - props.width = this.size.width + "px"; - } - if ( this.size.height !== this.prevSize.height ) { - props.height = this.size.height + "px"; - } +var uniqueId = $.fn.extend( { + uniqueId: ( function() { + var uuid = 0; - this.helper.css( props ); + return function() { + return this.each( function() { + if ( !this.id ) { + this.id = "ui-id-" + ( ++uuid ); + } + } ); + }; + } )(), - return props; - }, + removeUniqueId: function() { + return this.each( function() { + if ( /^ui-id-\d+$/.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + } ); + } +} ); - _updateVirtualBoundaries: function( forceAspectRatio ) { - var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b, - o = this.options; - b = { - minWidth: this._isNumber( o.minWidth ) ? o.minWidth : 0, - maxWidth: this._isNumber( o.maxWidth ) ? o.maxWidth : Infinity, - minHeight: this._isNumber( o.minHeight ) ? o.minHeight : 0, - maxHeight: this._isNumber( o.maxHeight ) ? o.maxHeight : Infinity - }; +/*! + * jQuery UI Accordion 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( this._aspectRatio || forceAspectRatio ) { - pMinWidth = b.minHeight * this.aspectRatio; - pMinHeight = b.minWidth / this.aspectRatio; - pMaxWidth = b.maxHeight * this.aspectRatio; - pMaxHeight = b.maxWidth / this.aspectRatio; +//>>label: Accordion +//>>group: Widgets +/* eslint-disable max-len */ +//>>description: Displays collapsible content panels for presenting information in a limited amount of space. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/accordion/ +//>>demos: http://jqueryui.com/accordion/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/accordion.css +//>>css.theme: ../../themes/base/theme.css - if ( pMinWidth > b.minWidth ) { - b.minWidth = pMinWidth; - } - if ( pMinHeight > b.minHeight ) { - b.minHeight = pMinHeight; - } - if ( pMaxWidth < b.maxWidth ) { - b.maxWidth = pMaxWidth; - } - if ( pMaxHeight < b.maxHeight ) { - b.maxHeight = pMaxHeight; - } - } - this._vBoundaries = b; + +var widgetsAccordion = $.widget( "ui.accordion", { + version: "1.13.2", + options: { + active: 0, + animate: {}, + classes: { + "ui-accordion-header": "ui-corner-top", + "ui-accordion-header-collapsed": "ui-corner-all", + "ui-accordion-content": "ui-corner-bottom" + }, + collapsible: false, + event: "click", + header: function( elem ) { + return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); + }, + heightStyle: "auto", + icons: { + activeHeader: "ui-icon-triangle-1-s", + header: "ui-icon-triangle-1-e" + }, + + // Callbacks + activate: null, + beforeActivate: null }, - _updateCache: function( data ) { - this.offset = this.helper.offset(); - if ( this._isNumber( data.left ) ) { - this.position.left = data.left; - } - if ( this._isNumber( data.top ) ) { - this.position.top = data.top; - } - if ( this._isNumber( data.height ) ) { - this.size.height = data.height; - } - if ( this._isNumber( data.width ) ) { - this.size.width = data.width; - } + hideProps: { + borderTopWidth: "hide", + borderBottomWidth: "hide", + paddingTop: "hide", + paddingBottom: "hide", + height: "hide" }, - _updateRatio: function( data ) { + showProps: { + borderTopWidth: "show", + borderBottomWidth: "show", + paddingTop: "show", + paddingBottom: "show", + height: "show" + }, - var cpos = this.position, - csize = this.size, - a = this.axis; + _create: function() { + var options = this.options; - if ( this._isNumber( data.height ) ) { - data.width = ( data.height * this.aspectRatio ); - } else if ( this._isNumber( data.width ) ) { - data.height = ( data.width / this.aspectRatio ); - } + this.prevShow = this.prevHide = $(); + this._addClass( "ui-accordion", "ui-widget ui-helper-reset" ); + this.element.attr( "role", "tablist" ); - if ( a === "sw" ) { - data.left = cpos.left + ( csize.width - data.width ); - data.top = null; - } - if ( a === "nw" ) { - data.top = cpos.top + ( csize.height - data.height ); - data.left = cpos.left + ( csize.width - data.width ); + // Don't allow collapsible: false and active: false / null + if ( !options.collapsible && ( options.active === false || options.active == null ) ) { + options.active = 0; } - return data; - }, + this._processPanels(); - _respectSize: function( data ) { - - var o = this._vBoundaries, - a = this.axis, - ismaxw = this._isNumber( data.width ) && o.maxWidth && ( o.maxWidth < data.width ), - ismaxh = this._isNumber( data.height ) && o.maxHeight && ( o.maxHeight < data.height ), - isminw = this._isNumber( data.width ) && o.minWidth && ( o.minWidth > data.width ), - isminh = this._isNumber( data.height ) && o.minHeight && ( o.minHeight > data.height ), - dw = this.originalPosition.left + this.originalSize.width, - dh = this.originalPosition.top + this.originalSize.height, - cw = /sw|nw|w/.test( a ), ch = /nw|ne|n/.test( a ); - if ( isminw ) { - data.width = o.minWidth; - } - if ( isminh ) { - data.height = o.minHeight; - } - if ( ismaxw ) { - data.width = o.maxWidth; - } - if ( ismaxh ) { - data.height = o.maxHeight; + // handle negative values + if ( options.active < 0 ) { + options.active += this.headers.length; } + this._refresh(); + }, - if ( isminw && cw ) { - data.left = dw - o.minWidth; - } - if ( ismaxw && cw ) { - data.left = dw - o.maxWidth; - } - if ( isminh && ch ) { - data.top = dh - o.minHeight; - } - if ( ismaxh && ch ) { - data.top = dh - o.maxHeight; - } + _getCreateEventData: function() { + return { + header: this.active, + panel: !this.active.length ? $() : this.active.next() + }; + }, - // Fixing jump error on top/left - bug #2330 - if ( !data.width && !data.height && !data.left && data.top ) { - data.top = null; - } else if ( !data.width && !data.height && !data.top && data.left ) { - data.left = null; + _createIcons: function() { + var icon, children, + icons = this.options.icons; + + if ( icons ) { + icon = $( "<span>" ); + this._addClass( icon, "ui-accordion-header-icon", "ui-icon " + icons.header ); + icon.prependTo( this.headers ); + children = this.active.children( ".ui-accordion-header-icon" ); + this._removeClass( children, icons.header ) + ._addClass( children, null, icons.activeHeader ) + ._addClass( this.headers, "ui-accordion-icons" ); } + }, - return data; + _destroyIcons: function() { + this._removeClass( this.headers, "ui-accordion-icons" ); + this.headers.children( ".ui-accordion-header-icon" ).remove(); }, - _getPaddingPlusBorderDimensions: function( element ) { - var i = 0, - widths = [], - borders = [ - element.css( "borderTopWidth" ), - element.css( "borderRightWidth" ), - element.css( "borderBottomWidth" ), - element.css( "borderLeftWidth" ) - ], - paddings = [ - element.css( "paddingTop" ), - element.css( "paddingRight" ), - element.css( "paddingBottom" ), - element.css( "paddingLeft" ) - ]; + _destroy: function() { + var contents; - for ( ; i < 4; i++ ) { - widths[ i ] = ( parseFloat( borders[ i ] ) || 0 ); - widths[ i ] += ( parseFloat( paddings[ i ] ) || 0 ); - } + // Clean up main element + this.element.removeAttr( "role" ); - return { - height: widths[ 0 ] + widths[ 2 ], - width: widths[ 1 ] + widths[ 3 ] - }; - }, + // Clean up headers + this.headers + .removeAttr( "role aria-expanded aria-selected aria-controls tabIndex" ) + .removeUniqueId(); - _proportionallyResize: function() { + this._destroyIcons(); - if ( !this._proportionallyResizeElements.length ) { - return; - } + // Clean up content panels + contents = this.headers.next() + .css( "display", "" ) + .removeAttr( "role aria-hidden aria-labelledby" ) + .removeUniqueId(); - var prel, - i = 0, - element = this.helper || this.element; + if ( this.options.heightStyle !== "content" ) { + contents.css( "height", "" ); + } + }, - for ( ; i < this._proportionallyResizeElements.length; i++ ) { + _setOption: function( key, value ) { + if ( key === "active" ) { - prel = this._proportionallyResizeElements[ i ]; + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } - // TODO: Seems like a bug to cache this.outerDimensions - // considering that we are in a loop. - if ( !this.outerDimensions ) { - this.outerDimensions = this._getPaddingPlusBorderDimensions( prel ); + if ( key === "event" ) { + if ( this.options.event ) { + this._off( this.headers, this.options.event ); } + this._setupEvents( value ); + } - prel.css( { - height: ( element.height() - this.outerDimensions.height ) || 0, - width: ( element.width() - this.outerDimensions.width ) || 0 - } ); + this._super( key, value ); + // Setting collapsible: false while collapsed; open first panel + if ( key === "collapsible" && !value && this.options.active === false ) { + this._activate( 0 ); } + if ( key === "icons" ) { + this._destroyIcons(); + if ( value ) { + this._createIcons(); + } + } }, - _renderProxy: function() { - - var el = this.element, o = this.options; - this.elementOffset = el.offset(); + _setOptionDisabled: function( value ) { + this._super( value ); - if ( this._helper ) { + this.element.attr( "aria-disabled", value ); - this.helper = this.helper || $( "<div></div>" ).css( { overflow: "hidden" } ); + // Support: IE8 Only + // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE + // so we need to add the disabled class to the headers and panels + this._toggleClass( null, "ui-state-disabled", !!value ); + this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", + !!value ); + }, - this._addClass( this.helper, this._helper ); - this.helper.css( { - width: this.element.outerWidth(), - height: this.element.outerHeight(), - position: "absolute", - left: this.elementOffset.left + "px", - top: this.elementOffset.top + "px", - zIndex: ++o.zIndex //TODO: Don't modify option - } ); + _keydown: function( event ) { + if ( event.altKey || event.ctrlKey ) { + return; + } - this.helper - .appendTo( "body" ) - .disableSelection(); + var keyCode = $.ui.keyCode, + length = this.headers.length, + currentIndex = this.headers.index( event.target ), + toFocus = false; - } else { - this.helper = this.element; + switch ( event.keyCode ) { + case keyCode.RIGHT: + case keyCode.DOWN: + toFocus = this.headers[ ( currentIndex + 1 ) % length ]; + break; + case keyCode.LEFT: + case keyCode.UP: + toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; + break; + case keyCode.SPACE: + case keyCode.ENTER: + this._eventHandler( event ); + break; + case keyCode.HOME: + toFocus = this.headers[ 0 ]; + break; + case keyCode.END: + toFocus = this.headers[ length - 1 ]; + break; } - }, - - _change: { - e: function( event, dx ) { - return { width: this.originalSize.width + dx }; - }, - w: function( event, dx ) { - var cs = this.originalSize, sp = this.originalPosition; - return { left: sp.left + dx, width: cs.width - dx }; - }, - n: function( event, dx, dy ) { - var cs = this.originalSize, sp = this.originalPosition; - return { top: sp.top + dy, height: cs.height - dy }; - }, - s: function( event, dx, dy ) { - return { height: this.originalSize.height + dy }; - }, - se: function( event, dx, dy ) { - return $.extend( this._change.s.apply( this, arguments ), - this._change.e.apply( this, [ event, dx, dy ] ) ); - }, - sw: function( event, dx, dy ) { - return $.extend( this._change.s.apply( this, arguments ), - this._change.w.apply( this, [ event, dx, dy ] ) ); - }, - ne: function( event, dx, dy ) { - return $.extend( this._change.n.apply( this, arguments ), - this._change.e.apply( this, [ event, dx, dy ] ) ); - }, - nw: function( event, dx, dy ) { - return $.extend( this._change.n.apply( this, arguments ), - this._change.w.apply( this, [ event, dx, dy ] ) ); + if ( toFocus ) { + $( event.target ).attr( "tabIndex", -1 ); + $( toFocus ).attr( "tabIndex", 0 ); + $( toFocus ).trigger( "focus" ); + event.preventDefault(); } }, - _propagate: function( n, event ) { - $.ui.plugin.call( this, n, [ event, this.ui() ] ); - if ( n !== "resize" ) { - this._trigger( n, event, this.ui() ); + _panelKeyDown: function( event ) { + if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + $( event.currentTarget ).prev().trigger( "focus" ); } }, - plugins: {}, + refresh: function() { + var options = this.options; + this._processPanels(); - ui: function() { - return { - originalElement: this.originalElement, - element: this.element, - helper: this.helper, - position: this.position, - size: this.size, - originalSize: this.originalSize, - originalPosition: this.originalPosition - }; - } - -} ); - -/* - * Resizable Extensions - */ - -$.ui.plugin.add( "resizable", "animate", { - - stop: function( event ) { - var that = $( this ).resizable( "instance" ), - o = that.options, - pr = that._proportionallyResizeElements, - ista = pr.length && ( /textarea/i ).test( pr[ 0 ].nodeName ), - soffseth = ista && that._hasScroll( pr[ 0 ], "left" ) ? 0 : that.sizeDiff.height, - soffsetw = ista ? 0 : that.sizeDiff.width, - style = { - width: ( that.size.width - soffsetw ), - height: ( that.size.height - soffseth ) - }, - left = ( parseFloat( that.element.css( "left" ) ) + - ( that.position.left - that.originalPosition.left ) ) || null, - top = ( parseFloat( that.element.css( "top" ) ) + - ( that.position.top - that.originalPosition.top ) ) || null; - - that.element.animate( - $.extend( style, top && left ? { top: top, left: left } : {} ), { - duration: o.animateDuration, - easing: o.animateEasing, - step: function() { + // Was collapsed or no panel + if ( ( options.active === false && options.collapsible === true ) || + !this.headers.length ) { + options.active = false; + this.active = $(); - var data = { - width: parseFloat( that.element.css( "width" ) ), - height: parseFloat( that.element.css( "height" ) ), - top: parseFloat( that.element.css( "top" ) ), - left: parseFloat( that.element.css( "left" ) ) - }; + // active false only when collapsible is true + } else if ( options.active === false ) { + this._activate( 0 ); - if ( pr && pr.length ) { - $( pr[ 0 ] ).css( { width: data.width, height: data.height } ); - } + // was active, but active panel is gone + } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { - // Propagating resize, and updating values for each animation step - that._updateCache( data ); - that._propagate( "resize", event ); + // all remaining panel are disabled + if ( this.headers.length === this.headers.find( ".ui-state-disabled" ).length ) { + options.active = false; + this.active = $(); - } + // activate previous panel + } else { + this._activate( Math.max( 0, options.active - 1 ) ); } - ); - } - -} ); - -$.ui.plugin.add( "resizable", "containment", { - - start: function() { - var element, p, co, ch, cw, width, height, - that = $( this ).resizable( "instance" ), - o = that.options, - el = that.element, - oc = o.containment, - ce = ( oc instanceof $ ) ? - oc.get( 0 ) : - ( /parent/.test( oc ) ) ? el.parent().get( 0 ) : oc; - - if ( !ce ) { - return; - } - - that.containerElement = $( ce ); - - if ( /document/.test( oc ) || oc === document ) { - that.containerOffset = { - left: 0, - top: 0 - }; - that.containerPosition = { - left: 0, - top: 0 - }; - that.parentData = { - element: $( document ), - left: 0, - top: 0, - width: $( document ).width(), - height: $( document ).height() || document.body.parentNode.scrollHeight - }; + // was active, active panel still exists } else { - element = $( ce ); - p = []; - $( [ "Top", "Right", "Left", "Bottom" ] ).each( function( i, name ) { - p[ i ] = that._num( element.css( "padding" + name ) ); - } ); - that.containerOffset = element.offset(); - that.containerPosition = element.position(); - that.containerSize = { - height: ( element.innerHeight() - p[ 3 ] ), - width: ( element.innerWidth() - p[ 1 ] ) - }; + // make sure active index is correct + options.active = this.headers.index( this.active ); + } - co = that.containerOffset; - ch = that.containerSize.height; - cw = that.containerSize.width; - width = ( that._hasScroll( ce, "left" ) ? ce.scrollWidth : cw ); - height = ( that._hasScroll( ce ) ? ce.scrollHeight : ch ); + this._destroyIcons(); - that.parentData = { - element: ce, - left: co.left, - top: co.top, - width: width, - height: height - }; - } + this._refresh(); }, - resize: function( event ) { - var woset, hoset, isParent, isOffsetRelative, - that = $( this ).resizable( "instance" ), - o = that.options, - co = that.containerOffset, - cp = that.position, - pRatio = that._aspectRatio || event.shiftKey, - cop = { - top: 0, - left: 0 - }, - ce = that.containerElement, - continueResize = true; + _processPanels: function() { + var prevHeaders = this.headers, + prevPanels = this.panels; - if ( ce[ 0 ] !== document && ( /static/ ).test( ce.css( "position" ) ) ) { - cop = co; + if ( typeof this.options.header === "function" ) { + this.headers = this.options.header( this.element ); + } else { + this.headers = this.element.find( this.options.header ); } + this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", + "ui-state-default" ); - if ( cp.left < ( that._helper ? co.left : 0 ) ) { - that.size.width = that.size.width + - ( that._helper ? - ( that.position.left - co.left ) : - ( that.position.left - cop.left ) ); + this.panels = this.headers.next().filter( ":not(.ui-accordion-content-active)" ).hide(); + this._addClass( this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content" ); - if ( pRatio ) { - that.size.height = that.size.width / that.aspectRatio; - continueResize = false; - } - that.position.left = o.helper ? co.left : 0; + // Avoid memory leaks (#10056) + if ( prevPanels ) { + this._off( prevHeaders.not( this.headers ) ); + this._off( prevPanels.not( this.panels ) ); } + }, - if ( cp.top < ( that._helper ? co.top : 0 ) ) { - that.size.height = that.size.height + - ( that._helper ? - ( that.position.top - co.top ) : - that.position.top ); + _refresh: function() { + var maxHeight, + options = this.options, + heightStyle = options.heightStyle, + parent = this.element.parent(); - if ( pRatio ) { - that.size.width = that.size.height * that.aspectRatio; - continueResize = false; - } - that.position.top = that._helper ? co.top : 0; - } + this.active = this._findActive( options.active ); + this._addClass( this.active, "ui-accordion-header-active", "ui-state-active" ) + ._removeClass( this.active, "ui-accordion-header-collapsed" ); + this._addClass( this.active.next(), "ui-accordion-content-active" ); + this.active.next().show(); - isParent = that.containerElement.get( 0 ) === that.element.parent().get( 0 ); - isOffsetRelative = /relative|absolute/.test( that.containerElement.css( "position" ) ); + this.headers + .attr( "role", "tab" ) + .each( function() { + var header = $( this ), + headerId = header.uniqueId().attr( "id" ), + panel = header.next(), + panelId = panel.uniqueId().attr( "id" ); + header.attr( "aria-controls", panelId ); + panel.attr( "aria-labelledby", headerId ); + } ) + .next() + .attr( "role", "tabpanel" ); - if ( isParent && isOffsetRelative ) { - that.offset.left = that.parentData.left + that.position.left; - that.offset.top = that.parentData.top + that.position.top; + this.headers + .not( this.active ) + .attr( { + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + } ) + .next() + .attr( { + "aria-hidden": "true" + } ) + .hide(); + + // Make sure at least one header is in the tab order + if ( !this.active.length ) { + this.headers.eq( 0 ).attr( "tabIndex", 0 ); } else { - that.offset.left = that.element.offset().left; - that.offset.top = that.element.offset().top; + this.active.attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ) + .next() + .attr( { + "aria-hidden": "false" + } ); } - woset = Math.abs( that.sizeDiff.width + - ( that._helper ? - that.offset.left - cop.left : - ( that.offset.left - co.left ) ) ); + this._createIcons(); - hoset = Math.abs( that.sizeDiff.height + - ( that._helper ? - that.offset.top - cop.top : - ( that.offset.top - co.top ) ) ); + this._setupEvents( options.event ); - if ( woset + that.size.width >= that.parentData.width ) { - that.size.width = that.parentData.width - woset; - if ( pRatio ) { - that.size.height = that.size.width / that.aspectRatio; - continueResize = false; - } - } + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + this.element.siblings( ":visible" ).each( function() { + var elem = $( this ), + position = elem.css( "position" ); - if ( hoset + that.size.height >= that.parentData.height ) { - that.size.height = that.parentData.height - hoset; - if ( pRatio ) { - that.size.width = that.size.height * that.aspectRatio; - continueResize = false; - } - } + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + } ); - if ( !continueResize ) { - that.position.left = that.prevPosition.left; - that.position.top = that.prevPosition.top; - that.size.width = that.prevSize.width; - that.size.height = that.prevSize.height; + this.headers.each( function() { + maxHeight -= $( this ).outerHeight( true ); + } ); + + this.headers.next() + .each( function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + } ) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.headers.next() + .each( function() { + var isVisible = $( this ).is( ":visible" ); + if ( !isVisible ) { + $( this ).show(); + } + maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); + if ( !isVisible ) { + $( this ).hide(); + } + } ) + .height( maxHeight ); } }, - stop: function() { - var that = $( this ).resizable( "instance" ), - o = that.options, - co = that.containerOffset, - cop = that.containerPosition, - ce = that.containerElement, - helper = $( that.helper ), - ho = helper.offset(), - w = helper.outerWidth() - that.sizeDiff.width, - h = helper.outerHeight() - that.sizeDiff.height; - - if ( that._helper && !o.animate && ( /relative/ ).test( ce.css( "position" ) ) ) { - $( this ).css( { - left: ho.left - cop.left - co.left, - width: w, - height: h - } ); - } + _activate: function( index ) { + var active = this._findActive( index )[ 0 ]; - if ( that._helper && !o.animate && ( /static/ ).test( ce.css( "position" ) ) ) { - $( this ).css( { - left: ho.left - cop.left - co.left, - width: w, - height: h - } ); + // Trying to activate the already active panel + if ( active === this.active[ 0 ] ) { + return; } - } -} ); - -$.ui.plugin.add( "resizable", "alsoResize", { - start: function() { - var that = $( this ).resizable( "instance" ), - o = that.options; + // Trying to collapse, simulate a click on the currently active header + active = active || this.active[ 0 ]; - $( o.alsoResize ).each( function() { - var el = $( this ); - el.data( "ui-resizable-alsoresize", { - width: parseFloat( el.width() ), height: parseFloat( el.height() ), - left: parseFloat( el.css( "left" ) ), top: parseFloat( el.css( "top" ) ) - } ); + this._eventHandler( { + target: active, + currentTarget: active, + preventDefault: $.noop } ); }, - resize: function( event, ui ) { - var that = $( this ).resizable( "instance" ), - o = that.options, - os = that.originalSize, - op = that.originalPosition, - delta = { - height: ( that.size.height - os.height ) || 0, - width: ( that.size.width - os.width ) || 0, - top: ( that.position.top - op.top ) || 0, - left: ( that.position.left - op.left ) || 0 - }; - - $( o.alsoResize ).each( function() { - var el = $( this ), start = $( this ).data( "ui-resizable-alsoresize" ), style = {}, - css = el.parents( ui.originalElement[ 0 ] ).length ? - [ "width", "height" ] : - [ "width", "height", "top", "left" ]; - - $.each( css, function( i, prop ) { - var sum = ( start[ prop ] || 0 ) + ( delta[ prop ] || 0 ); - if ( sum && sum >= 0 ) { - style[ prop ] = sum || null; - } - } ); + _findActive: function( selector ) { + return typeof selector === "number" ? this.headers.eq( selector ) : $(); + }, - el.css( style ); + _setupEvents: function( event ) { + var events = { + keydown: "_keydown" + }; + if ( event ) { + $.each( event.split( " " ), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; } ); + } + + this._off( this.headers.add( this.headers.next() ) ); + this._on( this.headers, events ); + this._on( this.headers.next(), { keydown: "_panelKeyDown" } ); + this._hoverable( this.headers ); + this._focusable( this.headers ); }, - stop: function() { - $( this ).removeData( "ui-resizable-alsoresize" ); - } -} ); + _eventHandler: function( event ) { + var activeChildren, clickedChildren, + options = this.options, + active = this.active, + clicked = $( event.currentTarget ), + clickedIsActive = clicked[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : clicked.next(), + toHide = active.next(), + eventData = { + oldHeader: active, + oldPanel: toHide, + newHeader: collapsing ? $() : clicked, + newPanel: toShow + }; -$.ui.plugin.add( "resizable", "ghost", { + event.preventDefault(); - start: function() { + if ( - var that = $( this ).resizable( "instance" ), cs = that.size; + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || - that.ghost = that.originalElement.clone(); - that.ghost.css( { - opacity: 0.25, - display: "block", - position: "relative", - height: cs.height, - width: cs.width, - margin: 0, - left: 0, - top: 0 - } ); + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } - that._addClass( that.ghost, "ui-resizable-ghost" ); + options.active = collapsing ? false : this.headers.index( clicked ); - // DEPRECATED - // TODO: remove after 1.12 - if ( $.uiBackCompat !== false && typeof that.options.ghost === "string" ) { + // When the call to ._toggle() comes after the class changes + // it causes a very odd bug in IE 8 (see #6720) + this.active = clickedIsActive ? $() : clicked; + this._toggle( eventData ); - // Ghost option - that.ghost.addClass( this.options.ghost ); + // Switch classes + // corner classes on the previously active header stay after the animation + this._removeClass( active, "ui-accordion-header-active", "ui-state-active" ); + if ( options.icons ) { + activeChildren = active.children( ".ui-accordion-header-icon" ); + this._removeClass( activeChildren, null, options.icons.activeHeader ) + ._addClass( activeChildren, null, options.icons.header ); } - that.ghost.appendTo( that.helper ); - - }, + if ( !clickedIsActive ) { + this._removeClass( clicked, "ui-accordion-header-collapsed" ) + ._addClass( clicked, "ui-accordion-header-active", "ui-state-active" ); + if ( options.icons ) { + clickedChildren = clicked.children( ".ui-accordion-header-icon" ); + this._removeClass( clickedChildren, null, options.icons.header ) + ._addClass( clickedChildren, null, options.icons.activeHeader ); + } - resize: function() { - var that = $( this ).resizable( "instance" ); - if ( that.ghost ) { - that.ghost.css( { - position: "relative", - height: that.size.height, - width: that.size.width - } ); + this._addClass( clicked.next(), "ui-accordion-content-active" ); } }, - stop: function() { - var that = $( this ).resizable( "instance" ); - if ( that.ghost && that.helper ) { - that.helper.get( 0 ).removeChild( that.ghost.get( 0 ) ); - } - } + _toggle: function( data ) { + var toShow = data.newPanel, + toHide = this.prevShow.length ? this.prevShow : data.oldPanel; -} ); + // Handle activating a panel during the animation for another activation + this.prevShow.add( this.prevHide ).stop( true, true ); + this.prevShow = toShow; + this.prevHide = toHide; -$.ui.plugin.add( "resizable", "grid", { + if ( this.options.animate ) { + this._animate( toShow, toHide, data ); + } else { + toHide.hide(); + toShow.show(); + this._toggleComplete( data ); + } - resize: function() { - var outerDimensions, - that = $( this ).resizable( "instance" ), - o = that.options, - cs = that.size, - os = that.originalSize, - op = that.originalPosition, - a = that.axis, - grid = typeof o.grid === "number" ? [ o.grid, o.grid ] : o.grid, - gridX = ( grid[ 0 ] || 1 ), - gridY = ( grid[ 1 ] || 1 ), - ox = Math.round( ( cs.width - os.width ) / gridX ) * gridX, - oy = Math.round( ( cs.height - os.height ) / gridY ) * gridY, - newWidth = os.width + ox, - newHeight = os.height + oy, - isMaxWidth = o.maxWidth && ( o.maxWidth < newWidth ), - isMaxHeight = o.maxHeight && ( o.maxHeight < newHeight ), - isMinWidth = o.minWidth && ( o.minWidth > newWidth ), - isMinHeight = o.minHeight && ( o.minHeight > newHeight ); + toHide.attr( { + "aria-hidden": "true" + } ); + toHide.prev().attr( { + "aria-selected": "false", + "aria-expanded": "false" + } ); - o.grid = grid; + // if we're switching panels, remove the old header from the tab order + // if we're opening from collapsed state, remove the previous header from the tab order + // if we're collapsing, then keep the collapsing header in the tab order + if ( toShow.length && toHide.length ) { + toHide.prev().attr( { + "tabIndex": -1, + "aria-expanded": "false" + } ); + } else if ( toShow.length ) { + this.headers.filter( function() { + return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; + } ) + .attr( "tabIndex", -1 ); + } - if ( isMinWidth ) { - newWidth += gridX; + toShow + .attr( "aria-hidden", "false" ) + .prev() + .attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + }, + + _animate: function( toShow, toHide, data ) { + var total, easing, duration, + that = this, + adjust = 0, + boxSizing = toShow.css( "box-sizing" ), + down = toShow.length && + ( !toHide.length || ( toShow.index() < toHide.index() ) ), + animate = this.options.animate || {}, + options = down && animate.down || animate, + complete = function() { + that._toggleComplete( data ); + }; + + if ( typeof options === "number" ) { + duration = options; } - if ( isMinHeight ) { - newHeight += gridY; + if ( typeof options === "string" ) { + easing = options; } - if ( isMaxWidth ) { - newWidth -= gridX; + + // fall back from options to animation in case of partial down settings + easing = easing || options.easing || animate.easing; + duration = duration || options.duration || animate.duration; + + if ( !toHide.length ) { + return toShow.animate( this.showProps, duration, easing, complete ); } - if ( isMaxHeight ) { - newHeight -= gridY; + if ( !toShow.length ) { + return toHide.animate( this.hideProps, duration, easing, complete ); } - if ( /^(se|s|e)$/.test( a ) ) { - that.size.width = newWidth; - that.size.height = newHeight; - } else if ( /^(ne)$/.test( a ) ) { - that.size.width = newWidth; - that.size.height = newHeight; - that.position.top = op.top - oy; - } else if ( /^(sw)$/.test( a ) ) { - that.size.width = newWidth; - that.size.height = newHeight; - that.position.left = op.left - ox; - } else { - if ( newHeight - gridY <= 0 || newWidth - gridX <= 0 ) { - outerDimensions = that._getPaddingPlusBorderDimensions( this ); + total = toShow.show().outerHeight(); + toHide.animate( this.hideProps, { + duration: duration, + easing: easing, + step: function( now, fx ) { + fx.now = Math.round( now ); } + } ); + toShow + .hide() + .animate( this.showProps, { + duration: duration, + easing: easing, + complete: complete, + step: function( now, fx ) { + fx.now = Math.round( now ); + if ( fx.prop !== "height" ) { + if ( boxSizing === "content-box" ) { + adjust += fx.now; + } + } else if ( that.options.heightStyle !== "content" ) { + fx.now = Math.round( total - toHide.outerHeight() - adjust ); + adjust = 0; + } + } + } ); + }, - if ( newHeight - gridY > 0 ) { - that.size.height = newHeight; - that.position.top = op.top - oy; - } else { - newHeight = gridY - outerDimensions.height; - that.size.height = newHeight; - that.position.top = op.top + os.height - newHeight; - } - if ( newWidth - gridX > 0 ) { - that.size.width = newWidth; - that.position.left = op.left - ox; - } else { - newWidth = gridX - outerDimensions.width; - that.size.width = newWidth; - that.position.left = op.left + os.width - newWidth; - } + _toggleComplete: function( data ) { + var toHide = data.oldPanel, + prev = toHide.prev(); + + this._removeClass( toHide, "ui-accordion-content-active" ); + this._removeClass( prev, "ui-accordion-header-active" ) + ._addClass( prev, "ui-accordion-header-collapsed" ); + + // Work around for rendering bug in IE (#5421) + if ( toHide.length ) { + toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; } + this._trigger( "activate", null, data ); } - } ); -var widgetsResizable = $.ui.resizable; + + +var safeActiveElement = $.ui.safeActiveElement = function( document ) { + var activeElement; + + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe> + try { + activeElement = document.activeElement; + } catch ( error ) { + activeElement = document.body; + } + + // Support: IE 9 - 11 only + // IE may return null instead of an element + // Interestingly, this only seems to occur when NOT in an iframe + if ( !activeElement ) { + activeElement = document.body; + } + + // Support: IE 11 only + // IE11 returns a seemingly empty object in some cases when accessing + // document.activeElement from an <iframe> + if ( !activeElement.nodeName ) { + activeElement = document.body; + } + + return activeElement; +}; /*! - * jQuery UI Selectable 1.13.1 + * jQuery UI Menu 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4952,1890 +4980,1902 @@ var widgetsResizable = $.ui.resizable; * http://jquery.org/license */ -//>>label: Selectable -//>>group: Interactions -//>>description: Allows groups of elements to be selected with the mouse. -//>>docs: http://api.jqueryui.com/selectable/ -//>>demos: http://jqueryui.com/selectable/ -//>>css.structure: ../../themes/base/selectable.css +//>>label: Menu +//>>group: Widgets +//>>description: Creates nestable menus. +//>>docs: http://api.jqueryui.com/menu/ +//>>demos: http://jqueryui.com/menu/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/menu.css +//>>css.theme: ../../themes/base/theme.css -var widgetsSelectable = $.widget( "ui.selectable", $.ui.mouse, { - version: "1.13.1", +var widgetsMenu = $.widget( "ui.menu", { + version: "1.13.2", + defaultElement: "<ul>", + delay: 300, options: { - appendTo: "body", - autoRefresh: true, - distance: 0, - filter: "*", - tolerance: "touch", + icons: { + submenu: "ui-icon-caret-1-e" + }, + items: "> *", + menus: "ul", + position: { + my: "left top", + at: "right top" + }, + role: "menu", // Callbacks - selected: null, - selecting: null, - start: null, - stop: null, - unselected: null, - unselecting: null + blur: null, + focus: null, + select: null }, - _create: function() { - var that = this; - - this._addClass( "ui-selectable" ); - this.dragged = false; + _create: function() { + this.activeMenu = this.element; - // Cache selectee children based on filter - this.refresh = function() { - that.elementPos = $( that.element[ 0 ] ).offset(); - that.selectees = $( that.options.filter, that.element[ 0 ] ); - that._addClass( that.selectees, "ui-selectee" ); - that.selectees.each( function() { - var $this = $( this ), - selecteeOffset = $this.offset(), - pos = { - left: selecteeOffset.left - that.elementPos.left, - top: selecteeOffset.top - that.elementPos.top - }; - $.data( this, "selectable-item", { - element: this, - $element: $this, - left: pos.left, - top: pos.top, - right: pos.left + $this.outerWidth(), - bottom: pos.top + $this.outerHeight(), - startselected: false, - selected: $this.hasClass( "ui-selected" ), - selecting: $this.hasClass( "ui-selecting" ), - unselecting: $this.hasClass( "ui-unselecting" ) - } ); + // Flag used to prevent firing of the click handler + // as the event bubbles up through nested menus + this.mouseHandled = false; + this.lastMousePosition = { x: null, y: null }; + this.element + .uniqueId() + .attr( { + role: this.options.role, + tabIndex: 0 } ); - }; - this.refresh(); - this._mouseInit(); + this._addClass( "ui-menu", "ui-widget ui-widget-content" ); + this._on( { - this.helper = $( "<div>" ); - this._addClass( this.helper, "ui-selectable-helper" ); - }, - - _destroy: function() { - this.selectees.removeData( "selectable-item" ); - this._mouseDestroy(); - }, - - _mouseStart: function( event ) { - var that = this, - options = this.options; - - this.opos = [ event.pageX, event.pageY ]; - this.elementPos = $( this.element[ 0 ] ).offset(); - - if ( this.options.disabled ) { - return; - } + // Prevent focus from sticking to links inside menu after clicking + // them (focus should always stay on UL during navigation). + "mousedown .ui-menu-item": function( event ) { + event.preventDefault(); - this.selectees = $( options.filter, this.element[ 0 ] ); + this._activateItem( event ); + }, + "click .ui-menu-item": function( event ) { + var target = $( event.target ); + var active = $( $.ui.safeActiveElement( this.document[ 0 ] ) ); + if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) { + this.select( event ); - this._trigger( "start", event ); + // Only set the mouseHandled flag if the event will bubble, see #9469. + if ( !event.isPropagationStopped() ) { + this.mouseHandled = true; + } - $( options.appendTo ).append( this.helper ); + // Open submenu on click + if ( target.has( ".ui-menu" ).length ) { + this.expand( event ); + } else if ( !this.element.is( ":focus" ) && + active.closest( ".ui-menu" ).length ) { - // position helper (lasso) - this.helper.css( { - "left": event.pageX, - "top": event.pageY, - "width": 0, - "height": 0 - } ); + // Redirect focus to the menu + this.element.trigger( "focus", [ true ] ); - if ( options.autoRefresh ) { - this.refresh(); - } + // If the active item is on the top level, let it stay active. + // Otherwise, blur the active item since it is no longer visible. + if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) { + clearTimeout( this.timer ); + } + } + } + }, + "mouseenter .ui-menu-item": "_activateItem", + "mousemove .ui-menu-item": "_activateItem", + mouseleave: "collapseAll", + "mouseleave .ui-menu": "collapseAll", + focus: function( event, keepActiveItem ) { - this.selectees.filter( ".ui-selected" ).each( function() { - var selectee = $.data( this, "selectable-item" ); - selectee.startselected = true; - if ( !event.metaKey && !event.ctrlKey ) { - that._removeClass( selectee.$element, "ui-selected" ); - selectee.selected = false; - that._addClass( selectee.$element, "ui-unselecting" ); - selectee.unselecting = true; + // If there's already an active item, keep it active + // If not, activate the first item + var item = this.active || this._menuItems().first(); - // selectable UNSELECTING callback - that._trigger( "unselecting", event, { - unselecting: selectee.element + if ( !keepActiveItem ) { + this.focus( event, item ); + } + }, + blur: function( event ) { + this._delay( function() { + var notContained = !$.contains( + this.element[ 0 ], + $.ui.safeActiveElement( this.document[ 0 ] ) + ); + if ( notContained ) { + this.collapseAll( event ); + } } ); - } + }, + keydown: "_keydown" } ); - $( event.target ).parents().addBack().each( function() { - var doSelect, - selectee = $.data( this, "selectable-item" ); - if ( selectee ) { - doSelect = ( !event.metaKey && !event.ctrlKey ) || - !selectee.$element.hasClass( "ui-selected" ); - that._removeClass( selectee.$element, doSelect ? "ui-unselecting" : "ui-selected" ) - ._addClass( selectee.$element, doSelect ? "ui-selecting" : "ui-unselecting" ); - selectee.unselecting = !doSelect; - selectee.selecting = doSelect; - selectee.selected = doSelect; + this.refresh(); - // selectable (UN)SELECTING callback - if ( doSelect ) { - that._trigger( "selecting", event, { - selecting: selectee.element - } ); - } else { - that._trigger( "unselecting", event, { - unselecting: selectee.element - } ); + // Clicks outside of a menu collapse any open menus + this._on( this.document, { + click: function( event ) { + if ( this._closeOnDocumentClick( event ) ) { + this.collapseAll( event, true ); } - return false; + + // Reset the mouseHandled flag + this.mouseHandled = false; } } ); - }, - _mouseDrag: function( event ) { + _activateItem: function( event ) { - this.dragged = true; + // Ignore mouse events while typeahead is active, see #10458. + // Prevents focusing the wrong item when typeahead causes a scroll while the mouse + // is over an item in the menu + if ( this.previousFilter ) { + return; + } - if ( this.options.disabled ) { + // If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356) + if ( event.clientX === this.lastMousePosition.x && + event.clientY === this.lastMousePosition.y ) { return; } - var tmp, - that = this, - options = this.options, - x1 = this.opos[ 0 ], - y1 = this.opos[ 1 ], - x2 = event.pageX, - y2 = event.pageY; + this.lastMousePosition = { + x: event.clientX, + y: event.clientY + }; - if ( x1 > x2 ) { - tmp = x2; x2 = x1; x1 = tmp; + var actualTarget = $( event.target ).closest( ".ui-menu-item" ), + target = $( event.currentTarget ); + + // Ignore bubbled events on parent items, see #11641 + if ( actualTarget[ 0 ] !== target[ 0 ] ) { + return; } - if ( y1 > y2 ) { - tmp = y2; y2 = y1; y1 = tmp; + + // If the item is already active, there's nothing to do + if ( target.is( ".ui-state-active" ) ) { + return; } - this.helper.css( { left: x1, top: y1, width: x2 - x1, height: y2 - y1 } ); - this.selectees.each( function() { - var selectee = $.data( this, "selectable-item" ), - hit = false, - offset = {}; + // Remove ui-state-active class from siblings of the newly focused menu item + // to avoid a jump caused by adjacent elements both having a class with a border + this._removeClass( target.siblings().children( ".ui-state-active" ), + null, "ui-state-active" ); + this.focus( event, target ); + }, - //prevent helper from being selected if appendTo: selectable - if ( !selectee || selectee.element === that.element[ 0 ] ) { - return; + _destroy: function() { + var items = this.element.find( ".ui-menu-item" ) + .removeAttr( "role aria-disabled" ), + submenus = items.children( ".ui-menu-item-wrapper" ) + .removeUniqueId() + .removeAttr( "tabIndex role aria-haspopup" ); + + // Destroy (sub)menus + this.element + .removeAttr( "aria-activedescendant" ) + .find( ".ui-menu" ).addBack() + .removeAttr( "role aria-labelledby aria-expanded aria-hidden aria-disabled " + + "tabIndex" ) + .removeUniqueId() + .show(); + + submenus.children().each( function() { + var elem = $( this ); + if ( elem.data( "ui-menu-submenu-caret" ) ) { + elem.remove(); } + } ); + }, - offset.left = selectee.left + that.elementPos.left; - offset.right = selectee.right + that.elementPos.left; - offset.top = selectee.top + that.elementPos.top; - offset.bottom = selectee.bottom + that.elementPos.top; + _keydown: function( event ) { + var match, prev, character, skip, + preventDefault = true; - if ( options.tolerance === "touch" ) { - hit = ( !( offset.left > x2 || offset.right < x1 || offset.top > y2 || - offset.bottom < y1 ) ); - } else if ( options.tolerance === "fit" ) { - hit = ( offset.left > x1 && offset.right < x2 && offset.top > y1 && - offset.bottom < y2 ); + switch ( event.keyCode ) { + case $.ui.keyCode.PAGE_UP: + this.previousPage( event ); + break; + case $.ui.keyCode.PAGE_DOWN: + this.nextPage( event ); + break; + case $.ui.keyCode.HOME: + this._move( "first", "first", event ); + break; + case $.ui.keyCode.END: + this._move( "last", "last", event ); + break; + case $.ui.keyCode.UP: + this.previous( event ); + break; + case $.ui.keyCode.DOWN: + this.next( event ); + break; + case $.ui.keyCode.LEFT: + this.collapse( event ); + break; + case $.ui.keyCode.RIGHT: + if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { + this.expand( event ); } + break; + case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: + this._activate( event ); + break; + case $.ui.keyCode.ESCAPE: + this.collapse( event ); + break; + default: + preventDefault = false; + prev = this.previousFilter || ""; + skip = false; - if ( hit ) { + // Support number pad values + character = event.keyCode >= 96 && event.keyCode <= 105 ? + ( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode ); - // SELECT - if ( selectee.selected ) { - that._removeClass( selectee.$element, "ui-selected" ); - selectee.selected = false; - } - if ( selectee.unselecting ) { - that._removeClass( selectee.$element, "ui-unselecting" ); - selectee.unselecting = false; - } - if ( !selectee.selecting ) { - that._addClass( selectee.$element, "ui-selecting" ); - selectee.selecting = true; + clearTimeout( this.filterTimer ); - // selectable SELECTING callback - that._trigger( "selecting", event, { - selecting: selectee.element - } ); - } + if ( character === prev ) { + skip = true; } else { + character = prev + character; + } - // UNSELECT - if ( selectee.selecting ) { - if ( ( event.metaKey || event.ctrlKey ) && selectee.startselected ) { - that._removeClass( selectee.$element, "ui-selecting" ); - selectee.selecting = false; - that._addClass( selectee.$element, "ui-selected" ); - selectee.selected = true; - } else { - that._removeClass( selectee.$element, "ui-selecting" ); - selectee.selecting = false; - if ( selectee.startselected ) { - that._addClass( selectee.$element, "ui-unselecting" ); - selectee.unselecting = true; - } - - // selectable UNSELECTING callback - that._trigger( "unselecting", event, { - unselecting: selectee.element - } ); - } - } - if ( selectee.selected ) { - if ( !event.metaKey && !event.ctrlKey && !selectee.startselected ) { - that._removeClass( selectee.$element, "ui-selected" ); - selectee.selected = false; + match = this._filterMenuItems( character ); + match = skip && match.index( this.active.next() ) !== -1 ? + this.active.nextAll( ".ui-menu-item" ) : + match; - that._addClass( selectee.$element, "ui-unselecting" ); - selectee.unselecting = true; + // If no matches on the current filter, reset to the last character pressed + // to move down the menu to the first item that starts with that character + if ( !match.length ) { + character = String.fromCharCode( event.keyCode ); + match = this._filterMenuItems( character ); + } - // selectable UNSELECTING callback - that._trigger( "unselecting", event, { - unselecting: selectee.element - } ); - } - } + if ( match.length ) { + this.focus( event, match ); + this.previousFilter = character; + this.filterTimer = this._delay( function() { + delete this.previousFilter; + }, 1000 ); + } else { + delete this.previousFilter; } - } ); + } - return false; + if ( preventDefault ) { + event.preventDefault(); + } }, - _mouseStop: function( event ) { - var that = this; - - this.dragged = false; + _activate: function( event ) { + if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { + if ( this.active.children( "[aria-haspopup='true']" ).length ) { + this.expand( event ); + } else { + this.select( event ); + } + } + }, - $( ".ui-unselecting", this.element[ 0 ] ).each( function() { - var selectee = $.data( this, "selectable-item" ); - that._removeClass( selectee.$element, "ui-unselecting" ); - selectee.unselecting = false; - selectee.startselected = false; - that._trigger( "unselected", event, { - unselected: selectee.element - } ); - } ); - $( ".ui-selecting", this.element[ 0 ] ).each( function() { - var selectee = $.data( this, "selectable-item" ); - that._removeClass( selectee.$element, "ui-selecting" ) - ._addClass( selectee.$element, "ui-selected" ); - selectee.selecting = false; - selectee.selected = true; - selectee.startselected = true; - that._trigger( "selected", event, { - selected: selectee.element - } ); - } ); - this._trigger( "stop", event ); + refresh: function() { + var menus, items, newSubmenus, newItems, newWrappers, + that = this, + icon = this.options.icons.submenu, + submenus = this.element.find( this.options.menus ); - this.helper.remove(); + this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length ); - return false; - } + // Initialize nested menus + newSubmenus = submenus.filter( ":not(.ui-menu)" ) + .hide() + .attr( { + role: this.options.role, + "aria-hidden": "true", + "aria-expanded": "false" + } ) + .each( function() { + var menu = $( this ), + item = menu.prev(), + submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true ); -} ); + that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon ); + item + .attr( "aria-haspopup", "true" ) + .prepend( submenuCaret ); + menu.attr( "aria-labelledby", item.attr( "id" ) ); + } ); + this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" ); -/*! - * jQuery UI Sortable 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + menus = submenus.add( this.element ); + items = menus.find( this.options.items ); -//>>label: Sortable -//>>group: Interactions -//>>description: Enables items in a list to be sorted using the mouse. -//>>docs: http://api.jqueryui.com/sortable/ -//>>demos: http://jqueryui.com/sortable/ -//>>css.structure: ../../themes/base/sortable.css + // Initialize menu-items containing spaces and/or dashes only as dividers + items.not( ".ui-menu-item" ).each( function() { + var item = $( this ); + if ( that._isDivider( item ) ) { + that._addClass( item, "ui-menu-divider", "ui-widget-content" ); + } + } ); + // Don't refresh list items that are already adapted + newItems = items.not( ".ui-menu-item, .ui-menu-divider" ); + newWrappers = newItems.children() + .not( ".ui-menu" ) + .uniqueId() + .attr( { + tabIndex: -1, + role: this._itemRole() + } ); + this._addClass( newItems, "ui-menu-item" ) + ._addClass( newWrappers, "ui-menu-item-wrapper" ); -var widgetsSortable = $.widget( "ui.sortable", $.ui.mouse, { - version: "1.13.1", - widgetEventPrefix: "sort", - ready: false, - options: { - appendTo: "parent", - axis: false, - connectWith: false, - containment: false, - cursor: "auto", - cursorAt: false, - dropOnEmpty: true, - forcePlaceholderSize: false, - forceHelperSize: false, - grid: false, - handle: false, - helper: "original", - items: "> *", - opacity: false, - placeholder: false, - revert: false, - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - scope: "default", - tolerance: "intersect", - zIndex: 1000, + // Add aria-disabled attribute to any disabled menu item + items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); - // Callbacks - activate: null, - beforeStop: null, - change: null, - deactivate: null, - out: null, - over: null, - receive: null, - remove: null, - sort: null, - start: null, - stop: null, - update: null + // If the active item has been removed, blur the menu + if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + this.blur(); + } }, - _isOverAxis: function( x, reference, size ) { - return ( x >= reference ) && ( x < ( reference + size ) ); + _itemRole: function() { + return { + menu: "menuitem", + listbox: "option" + }[ this.options.role ]; }, - _isFloating: function( item ) { - return ( /left|right/ ).test( item.css( "float" ) ) || - ( /inline|table-cell/ ).test( item.css( "display" ) ); + _setOption: function( key, value ) { + if ( key === "icons" ) { + var icons = this.element.find( ".ui-menu-icon" ); + this._removeClass( icons, null, this.options.icons.submenu ) + ._addClass( icons, null, value.submenu ); + } + this._super( key, value ); }, - _create: function() { - this.containerCache = {}; - this._addClass( "ui-sortable" ); + _setOptionDisabled: function( value ) { + this._super( value ); - //Get the items - this.refresh(); + this.element.attr( "aria-disabled", String( value ) ); + this._toggleClass( null, "ui-state-disabled", !!value ); + }, - //Let's determine the parent's offset - this.offset = this.element.offset(); + focus: function( event, item ) { + var nested, focused, activeParent; + this.blur( event, event && event.type === "focus" ); - //Initialize mouse events for interaction - this._mouseInit(); + this._scrollIntoView( item ); - this._setHandleClassName(); + this.active = item.first(); - //We're ready to go - this.ready = true; - - }, - - _setOption: function( key, value ) { - this._super( key, value ); + focused = this.active.children( ".ui-menu-item-wrapper" ); + this._addClass( focused, null, "ui-state-active" ); - if ( key === "handle" ) { - this._setHandleClassName(); + // Only update aria-activedescendant if there's a role + // otherwise we assume focus is managed elsewhere + if ( this.options.role ) { + this.element.attr( "aria-activedescendant", focused.attr( "id" ) ); } - }, - - _setHandleClassName: function() { - var that = this; - this._removeClass( this.element.find( ".ui-sortable-handle" ), "ui-sortable-handle" ); - $.each( this.items, function() { - that._addClass( - this.instance.options.handle ? - this.item.find( this.instance.options.handle ) : - this.item, - "ui-sortable-handle" - ); - } ); - }, - _destroy: function() { - this._mouseDestroy(); + // Highlight active parent menu item, if any + activeParent = this.active + .parent() + .closest( ".ui-menu-item" ) + .children( ".ui-menu-item-wrapper" ); + this._addClass( activeParent, null, "ui-state-active" ); - for ( var i = this.items.length - 1; i >= 0; i-- ) { - this.items[ i ].item.removeData( this.widgetName + "-item" ); + if ( event && event.type === "keydown" ) { + this._close(); + } else { + this.timer = this._delay( function() { + this._close(); + }, this.delay ); } - return this; - }, - - _mouseCapture: function( event, overrideHandle ) { - var currentItem = null, - validHandle = false, - that = this; - - if ( this.reverting ) { - return false; + nested = item.children( ".ui-menu" ); + if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) { + this._startOpening( nested ); } + this.activeMenu = item.parent(); - if ( this.options.disabled || this.options.type === "static" ) { - return false; - } + this._trigger( "focus", event, { item: item } ); + }, - //We have to refresh the items data once first - this._refreshItems( event ); + _scrollIntoView: function( item ) { + var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; + if ( this._hasScroll() ) { + borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0; + paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0; + offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; + scroll = this.activeMenu.scrollTop(); + elementHeight = this.activeMenu.height(); + itemHeight = item.outerHeight(); - //Find out if the clicked node (or one of its parents) is a actual item in this.items - $( event.target ).parents().each( function() { - if ( $.data( this, that.widgetName + "-item" ) === that ) { - currentItem = $( this ); - return false; + if ( offset < 0 ) { + this.activeMenu.scrollTop( scroll + offset ); + } else if ( offset + itemHeight > elementHeight ) { + this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); } - } ); - if ( $.data( event.target, that.widgetName + "-item" ) === that ) { - currentItem = $( event.target ); } + }, - if ( !currentItem ) { - return false; + blur: function( event, fromFocus ) { + if ( !fromFocus ) { + clearTimeout( this.timer ); } - if ( this.options.handle && !overrideHandle ) { - $( this.options.handle, currentItem ).find( "*" ).addBack().each( function() { - if ( this === event.target ) { - validHandle = true; - } - } ); - if ( !validHandle ) { - return false; - } + + if ( !this.active ) { + return; } - this.currentItem = currentItem; - this._removeCurrentsFromItems(); - return true; + this._removeClass( this.active.children( ".ui-menu-item-wrapper" ), + null, "ui-state-active" ); + this._trigger( "blur", event, { item: this.active } ); + this.active = null; }, - _mouseStart: function( event, overrideHandle, noActivation ) { + _startOpening: function( submenu ) { + clearTimeout( this.timer ); - var i, body, - o = this.options; + // Don't open if already open fixes a Firefox bug that caused a .5 pixel + // shift in the submenu position when mousing over the caret icon + if ( submenu.attr( "aria-hidden" ) !== "true" ) { + return; + } - this.currentContainer = this; + this.timer = this._delay( function() { + this._close(); + this._open( submenu ); + }, this.delay ); + }, - //We only need to call refreshPositions, because the refreshItems call has been moved to - // mouseCapture - this.refreshPositions(); + _open: function( submenu ) { + var position = $.extend( { + of: this.active + }, this.options.position ); - //Prepare the dragged items parent - this.appendTo = $( o.appendTo !== "parent" ? - o.appendTo : - this.currentItem.parent() ); + clearTimeout( this.timer ); + this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) ) + .hide() + .attr( "aria-hidden", "true" ); - //Create and append the visible helper - this.helper = this._createHelper( event ); + submenu + .show() + .removeAttr( "aria-hidden" ) + .attr( "aria-expanded", "true" ) + .position( position ); + }, - //Cache the helper size - this._cacheHelperProportions(); + collapseAll: function( event, all ) { + clearTimeout( this.timer ); + this.timer = this._delay( function() { - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ + // If we were passed an event, look for the submenu that contains the event + var currentMenu = all ? this.element : + $( event && event.target ).closest( this.element.find( ".ui-menu" ) ); - //Cache the margins of the original element - this._cacheMargins(); + // If we found no valid submenu ancestor, use the main menu to close all + // sub menus anyway + if ( !currentMenu.length ) { + currentMenu = this.element; + } - //The element's absolute position on the page minus margins - this.offset = this.currentItem.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; + this._close( currentMenu ); - $.extend( this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, + this.blur( event ); - // This is a relative to absolute position minus the actual position calculation - - // only used for relative positioned helper - relative: this._getRelativeOffset() - } ); + // Work around active item staying active after menu is blurred + this._removeClass( currentMenu.find( ".ui-state-active" ), null, "ui-state-active" ); - // After we get the helper offset, but before we get the parent offset we can - // change the helper's position to absolute - // TODO: Still need to figure out a way to make relative sorting possible - this.helper.css( "position", "absolute" ); - this.cssPosition = this.helper.css( "position" ); + this.activeMenu = currentMenu; + }, all ? 0 : this.delay ); + }, - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - if ( o.cursorAt ) { - this._adjustOffsetFromHelper( o.cursorAt ); + // With no arguments, closes the currently active menu - if nothing is active + // it closes all menus. If passed an argument, it will search for menus BELOW + _close: function( startMenu ) { + if ( !startMenu ) { + startMenu = this.active ? this.active.parent() : this.element; } - //Cache the former DOM position - this.domPosition = { - prev: this.currentItem.prev()[ 0 ], - parent: this.currentItem.parent()[ 0 ] - }; - - // If the helper is not the original, hide the original so it's not playing any role during - // the drag, won't cause anything bad this way - if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { - this.currentItem.hide(); - } + startMenu.find( ".ui-menu" ) + .hide() + .attr( "aria-hidden", "true" ) + .attr( "aria-expanded", "false" ); + }, - //Create the placeholder - this._createPlaceholder(); + _closeOnDocumentClick: function( event ) { + return !$( event.target ).closest( ".ui-menu" ).length; + }, - //Get the next scrolling parent - this.scrollParent = this.placeholder.scrollParent(); + _isDivider: function( item ) { - $.extend( this.offset, { - parent: this._getParentOffset() - } ); + // Match hyphen, em dash, en dash + return !/[^\-\u2014\u2013\s]/.test( item.text() ); + }, - //Set a containment if given in the options - if ( o.containment ) { - this._setContainment(); + collapse: function( event ) { + var newItem = this.active && + this.active.parent().closest( ".ui-menu-item", this.element ); + if ( newItem && newItem.length ) { + this._close(); + this.focus( event, newItem ); } + }, - if ( o.cursor && o.cursor !== "auto" ) { // cursor option - body = this.document.find( "body" ); + expand: function( event ) { + var newItem = this.active && this._menuItems( this.active.children( ".ui-menu" ) ).first(); - // Support: IE - this.storedCursor = body.css( "cursor" ); - body.css( "cursor", o.cursor ); + if ( newItem && newItem.length ) { + this._open( newItem.parent() ); - this.storedStylesheet = - $( "<style>*{ cursor: " + o.cursor + " !important; }</style>" ).appendTo( body ); - } - - // We need to make sure to grab the zIndex before setting the - // opacity, because setting the opacity to anything lower than 1 - // causes the zIndex to change from "auto" to 0. - if ( o.zIndex ) { // zIndex option - if ( this.helper.css( "zIndex" ) ) { - this._storedZIndex = this.helper.css( "zIndex" ); - } - this.helper.css( "zIndex", o.zIndex ); + // Delay so Firefox will not hide activedescendant change in expanding submenu from AT + this._delay( function() { + this.focus( event, newItem ); + } ); } + }, - if ( o.opacity ) { // opacity option - if ( this.helper.css( "opacity" ) ) { - this._storedOpacity = this.helper.css( "opacity" ); - } - this.helper.css( "opacity", o.opacity ); - } + next: function( event ) { + this._move( "next", "first", event ); + }, - //Prepare scrolling - if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ].tagName !== "HTML" ) { - this.overflowOffset = this.scrollParent.offset(); - } + previous: function( event ) { + this._move( "prev", "last", event ); + }, - //Call callbacks - this._trigger( "start", event, this._uiHash() ); + isFirstItem: function() { + return this.active && !this.active.prevAll( ".ui-menu-item" ).length; + }, - //Recache the helper size - if ( !this._preserveHelperProportions ) { - this._cacheHelperProportions(); - } + isLastItem: function() { + return this.active && !this.active.nextAll( ".ui-menu-item" ).length; + }, - //Post "activate" events to possible containers - if ( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } + _menuItems: function( menu ) { + return ( menu || this.element ) + .find( this.options.items ) + .filter( ".ui-menu-item" ); + }, - //Prepare possible droppables - if ( $.ui.ddmanager ) { - $.ui.ddmanager.current = this; + _move: function( direction, filter, event ) { + var next; + if ( this.active ) { + if ( direction === "first" || direction === "last" ) { + next = this.active + [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) + .last(); + } else { + next = this.active + [ direction + "All" ]( ".ui-menu-item" ) + .first(); + } } - - if ( $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); + if ( !next || !next.length || !this.active ) { + next = this._menuItems( this.activeMenu )[ filter ](); } - this.dragging = true; - - this._addClass( this.helper, "ui-sortable-helper" ); + this.focus( event, next ); + }, - //Move the helper, if needed - if ( !this.helper.parent().is( this.appendTo ) ) { - this.helper.detach().appendTo( this.appendTo ); + nextPage: function( event ) { + var item, base, height; - //Update position - this.offset.parent = this._getParentOffset(); + if ( !this.active ) { + this.next( event ); + return; } + if ( this.isLastItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.innerHeight(); - //Generate the original position - this.position = this.originalPosition = this._generatePosition( event ); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - this.lastPositionAbs = this.positionAbs = this._convertPositionTo( "absolute" ); - - this._mouseDrag( event ); + // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. + if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { + height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); + } - return true; + this.active.nextAll( ".ui-menu-item" ).each( function() { + item = $( this ); + return item.offset().top - base - height < 0; + } ); + this.focus( event, item ); + } else { + this.focus( event, this._menuItems( this.activeMenu ) + [ !this.active ? "first" : "last" ]() ); + } }, - _scroll: function( event ) { - var o = this.options, - scrolled = false; - - if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ].tagName !== "HTML" ) { + previousPage: function( event ) { + var item, base, height; + if ( !this.active ) { + this.next( event ); + return; + } + if ( this.isFirstItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.innerHeight(); - if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) - - event.pageY < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollTop = - scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed; - } else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollTop = - scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed; + // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. + if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { + height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); } - if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) - - event.pageX < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollLeft = scrolled = - this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed; - } else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollLeft = scrolled = - this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed; - } + this.active.prevAll( ".ui-menu-item" ).each( function() { + item = $( this ); + return item.offset().top - base + height > 0; + } ); + this.focus( event, item ); } else { - - if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) { - scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed ); - } else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) < - o.scrollSensitivity ) { - scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed ); - } - - if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) { - scrolled = this.document.scrollLeft( - this.document.scrollLeft() - o.scrollSpeed - ); - } else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) < - o.scrollSensitivity ) { - scrolled = this.document.scrollLeft( - this.document.scrollLeft() + o.scrollSpeed - ); - } - + this.focus( event, this._menuItems( this.activeMenu ).first() ); } - - return scrolled; }, - _mouseDrag: function( event ) { - var i, item, itemElement, intersection, - o = this.options; + _hasScroll: function() { + return this.element.outerHeight() < this.element.prop( "scrollHeight" ); + }, - //Compute the helpers position - this.position = this._generatePosition( event ); - this.positionAbs = this._convertPositionTo( "absolute" ); + select: function( event ) { - //Set the helper position - if ( !this.options.axis || this.options.axis !== "y" ) { - this.helper[ 0 ].style.left = this.position.left + "px"; - } - if ( !this.options.axis || this.options.axis !== "x" ) { - this.helper[ 0 ].style.top = this.position.top + "px"; + // TODO: It should never be possible to not have an active item at this + // point, but the tests don't trigger mouseenter before click. + this.active = this.active || $( event.target ).closest( ".ui-menu-item" ); + var ui = { item: this.active }; + if ( !this.active.has( ".ui-menu" ).length ) { + this.collapseAll( event, true ); } + this._trigger( "select", event, ui ); + }, - //Do scrolling - if ( o.scroll ) { - if ( this._scroll( event ) !== false ) { + _filterMenuItems: function( character ) { + var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ), + regex = new RegExp( "^" + escapedCharacter, "i" ); - //Update item positions used in position checks - this._refreshItemPositions( true ); + return this.activeMenu + .find( this.options.items ) - if ( $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); - } - } - } + // Only match on items, not dividers or other content (#10571) + .filter( ".ui-menu-item" ) + .filter( function() { + return regex.test( + String.prototype.trim.call( + $( this ).children( ".ui-menu-item-wrapper" ).text() ) ); + } ); + } +} ); - this.dragDirection = { - vertical: this._getDragVerticalDirection(), - horizontal: this._getDragHorizontalDirection() - }; - //Rearrange - for ( i = this.items.length - 1; i >= 0; i-- ) { +/*! + * jQuery UI Autocomplete 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - //Cache variables and intersection, continue if no intersection - item = this.items[ i ]; - itemElement = item.item[ 0 ]; - intersection = this._intersectsWithPointer( item ); - if ( !intersection ) { - continue; - } +//>>label: Autocomplete +//>>group: Widgets +//>>description: Lists suggested words as the user is typing. +//>>docs: http://api.jqueryui.com/autocomplete/ +//>>demos: http://jqueryui.com/autocomplete/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/autocomplete.css +//>>css.theme: ../../themes/base/theme.css - // Only put the placeholder inside the current Container, skip all - // items from other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this, moving items in "sub-sortables" can cause - // the placeholder to jitter between the outer and inner container. - if ( item.instance !== this.currentContainer ) { - continue; - } - // Cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if ( itemElement !== this.currentItem[ 0 ] && - this.placeholder[ intersection === 1 ? - "next" : "prev" ]()[ 0 ] !== itemElement && - !$.contains( this.placeholder[ 0 ], itemElement ) && - ( this.options.type === "semi-dynamic" ? - !$.contains( this.element[ 0 ], itemElement ) : - true - ) - ) { +$.widget( "ui.autocomplete", { + version: "1.13.2", + defaultElement: "<input>", + options: { + appendTo: null, + autoFocus: false, + delay: 300, + minLength: 1, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null, - this.direction = intersection === 1 ? "down" : "up"; + // Callbacks + change: null, + close: null, + focus: null, + open: null, + response: null, + search: null, + select: null + }, - if ( this.options.tolerance === "pointer" || - this._intersectsWithSides( item ) ) { - this._rearrange( event, item ); - } else { - break; - } + requestIndex: 0, + pending: 0, + liveRegionTimer: null, - this._trigger( "change", event, this._uiHash() ); - break; - } - } + _create: function() { - //Post events to containers - this._contactContainers( event ); + // Some browsers only repeat keydown events, not keypress events, + // so we use the suppressKeyPress flag to determine if we've already + // handled the keydown event. #7269 + // Unfortunately the code for & in keypress is the same as the up arrow, + // so we use the suppressKeyPressRepeat flag to avoid handling keypress + // events when we know the keydown event was used to modify the + // search term. #7799 + var suppressKeyPress, suppressKeyPressRepeat, suppressInput, + nodeName = this.element[ 0 ].nodeName.toLowerCase(), + isTextarea = nodeName === "textarea", + isInput = nodeName === "input"; - //Interconnect with droppables - if ( $.ui.ddmanager ) { - $.ui.ddmanager.drag( this, event ); - } + // Textareas are always multi-line + // Inputs are always single-line, even if inside a contentEditable element + // IE also treats inputs as contentEditable + // All other element types are determined by whether or not they're contentEditable + this.isMultiLine = isTextarea || !isInput && this._isContentEditable( this.element ); - //Call callbacks - this._trigger( "sort", event, this._uiHash() ); + this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; + this.isNewMenu = true; - this.lastPositionAbs = this.positionAbs; - return false; + this._addClass( "ui-autocomplete-input" ); + this.element.attr( "autocomplete", "off" ); - }, + this._on( this.element, { + keydown: function( event ) { + if ( this.element.prop( "readOnly" ) ) { + suppressKeyPress = true; + suppressInput = true; + suppressKeyPressRepeat = true; + return; + } - _mouseStop: function( event, noPropagation ) { + suppressKeyPress = false; + suppressInput = false; + suppressKeyPressRepeat = false; + var keyCode = $.ui.keyCode; + switch ( event.keyCode ) { + case keyCode.PAGE_UP: + suppressKeyPress = true; + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + suppressKeyPress = true; + this._move( "nextPage", event ); + break; + case keyCode.UP: + suppressKeyPress = true; + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + suppressKeyPress = true; + this._keyEvent( "next", event ); + break; + case keyCode.ENTER: - if ( !event ) { - return; - } + // when menu is open and has focus + if ( this.menu.active ) { - //If we are using droppables, inform the manager about the drop - if ( $.ui.ddmanager && !this.options.dropBehaviour ) { - $.ui.ddmanager.drop( this, event ); - } + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; + event.preventDefault(); + this.menu.select( event ); + } + break; + case keyCode.TAB: + if ( this.menu.active ) { + this.menu.select( event ); + } + break; + case keyCode.ESCAPE: + if ( this.menu.element.is( ":visible" ) ) { + if ( !this.isMultiLine ) { + this._value( this.term ); + } + this.close( event ); - if ( this.options.revert ) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; + // Different browsers have different default behavior for escape + // Single press can mean undo or clear + // Double press in IE means clear the whole form + event.preventDefault(); + } + break; + default: + suppressKeyPressRepeat = true; - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + - ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? - 0 : - this.offsetParent[ 0 ].scrollLeft - ); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + - ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? - 0 : - this.offsetParent[ 0 ].scrollTop - ); - } - this.reverting = true; - $( this.helper ).animate( - animation, - parseInt( this.options.revert, 10 ) || 500, - function() { - that._clear( event ); + // search timeout should be triggered before the input value is changed + this._searchTimeout( event ); + break; + } + }, + keypress: function( event ) { + if ( suppressKeyPress ) { + suppressKeyPress = false; + if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { + event.preventDefault(); + } + return; + } + if ( suppressKeyPressRepeat ) { + return; } - ); - } else { - this._clear( event, noPropagation ); - } - return false; + // Replicate some key handlers to allow them to repeat in Firefox and Opera + var keyCode = $.ui.keyCode; + switch ( event.keyCode ) { + case keyCode.PAGE_UP: + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + this._move( "nextPage", event ); + break; + case keyCode.UP: + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + this._keyEvent( "next", event ); + break; + } + }, + input: function( event ) { + if ( suppressInput ) { + suppressInput = false; + event.preventDefault(); + return; + } + this._searchTimeout( event ); + }, + focus: function() { + this.selectedItem = null; + this.previous = this._value(); + }, + blur: function( event ) { + clearTimeout( this.searching ); + this.close( event ); + this._change( event ); + } + } ); - }, + this._initSource(); + this.menu = $( "<ul>" ) + .appendTo( this._appendTo() ) + .menu( { - cancel: function() { + // disable ARIA support, the live region takes care of that + role: null + } ) + .hide() - if ( this.dragging ) { + // Support: IE 11 only, Edge <= 14 + // For other browsers, we preventDefault() on the mousedown event + // to keep the dropdown from taking focus from the input. This doesn't + // work for IE/Edge, causing problems with selection and scrolling (#9638) + // Happily, IE and Edge support an "unselectable" attribute that + // prevents an element from receiving focus, exactly what we want here. + .attr( { + "unselectable": "on" + } ) + .menu( "instance" ); - this._mouseUp( new $.Event( "mouseup", { target: null } ) ); + this._addClass( this.menu.element, "ui-autocomplete", "ui-front" ); + this._on( this.menu.element, { + mousedown: function( event ) { - if ( this.options.helper === "original" ) { - this.currentItem.css( this._storedCSS ); - this._removeClass( this.currentItem, "ui-sortable-helper" ); - } else { - this.currentItem.show(); - } + // Prevent moving focus out of the text field + event.preventDefault(); + }, + menufocus: function( event, ui ) { + var label, item; - //Post deactivating events to containers - for ( var i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) ); - if ( this.containers[ i ].containerCache.over ) { - this.containers[ i ]._trigger( "out", null, this._uiHash( this ) ); - this.containers[ i ].containerCache.over = 0; - } - } + // support: Firefox + // Prevent accidental activation of menu items in Firefox (#7024 #9118) + if ( this.isNewMenu ) { + this.isNewMenu = false; + if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) { + this.menu.blur(); - } + this.document.one( "mousemove", function() { + $( event.target ).trigger( event.originalEvent ); + } ); - if ( this.placeholder ) { + return; + } + } - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, - // it unbinds ALL events from the original node! - if ( this.placeholder[ 0 ].parentNode ) { - this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); - } - if ( this.options.helper !== "original" && this.helper && - this.helper[ 0 ].parentNode ) { - this.helper.remove(); - } + item = ui.item.data( "ui-autocomplete-item" ); + if ( false !== this._trigger( "focus", event, { item: item } ) ) { - $.extend( this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - } ); + // use value to match what will end up in the input, if it was a key event + if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) { + this._value( item.value ); + } + } - if ( this.domPosition.prev ) { - $( this.domPosition.prev ).after( this.currentItem ); - } else { - $( this.domPosition.parent ).prepend( this.currentItem ); - } - } + // Announce the value in the liveRegion + label = ui.item.attr( "aria-label" ) || item.value; + if ( label && String.prototype.trim.call( label ).length ) { + clearTimeout( this.liveRegionTimer ); + this.liveRegionTimer = this._delay( function() { + this.liveRegion.html( $( "<div>" ).text( label ) ); + }, 100 ); + } + }, + menuselect: function( event, ui ) { + var item = ui.item.data( "ui-autocomplete-item" ), + previous = this.previous; - return this; + // Only trigger when focus was lost (click on menu) + if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) { + this.element.trigger( "focus" ); + this.previous = previous; - }, + // #6109 - IE triggers two focus events and the second + // is asynchronous, so we need to reset the previous + // term synchronously and asynchronously :-( + this._delay( function() { + this.previous = previous; + this.selectedItem = item; + } ); + } - serialize: function( o ) { + if ( false !== this._trigger( "select", event, { item: item } ) ) { + this._value( item.value ); + } - var items = this._getItemsAsjQuery( o && o.connected ), - str = []; - o = o || {}; + // reset the term after the select event + // this allows custom select handling to work properly + this.term = this._value(); - $( items ).each( function() { - var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" ) - .match( o.expression || ( /(.+)[\-=_](.+)/ ) ); - if ( res ) { - str.push( - ( o.key || res[ 1 ] + "[]" ) + - "=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) ); + this.close( event ); + this.selectedItem = item; } } ); - if ( !str.length && o.key ) { - str.push( o.key + "=" ); - } - - return str.join( "&" ); - - }, - - toArray: function( o ) { - - var items = this._getItemsAsjQuery( o && o.connected ), - ret = []; + this.liveRegion = $( "<div>", { + role: "status", + "aria-live": "assertive", + "aria-relevant": "additions" + } ) + .appendTo( this.document[ 0 ].body ); - o = o || {}; + this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" ); - items.each( function() { - ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" ); + // Turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } } ); - return ret; - }, - /* Be careful with the following core functions */ - _intersectsWith: function( item ) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && - ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && - ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - ( this.options.tolerance !== "pointer" && - this.helperProportions[ this.floating ? "width" : "height" ] > - item[ this.floating ? "width" : "height" ] ) - ) { - return isOverElement; - } else { - - return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half - x2 - ( this.helperProportions.width / 2 ) < r && // Left Half - t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half - y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half - - } + _destroy: function() { + clearTimeout( this.searching ); + this.element.removeAttr( "autocomplete" ); + this.menu.element.remove(); + this.liveRegion.remove(); }, - _intersectsWithPointer: function( item ) { - var verticalDirection, horizontalDirection, - isOverElementHeight = ( this.options.axis === "x" ) || - this._isOverAxis( - this.positionAbs.top + this.offset.click.top, item.top, item.height ), - isOverElementWidth = ( this.options.axis === "y" ) || - this._isOverAxis( - this.positionAbs.left + this.offset.click.left, item.left, item.width ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( !isOverElement ) { - return false; + _setOption: function( key, value ) { + this._super( key, value ); + if ( key === "source" ) { + this._initSource(); } + if ( key === "appendTo" ) { + this.menu.element.appendTo( this._appendTo() ); + } + if ( key === "disabled" && value && this.xhr ) { + this.xhr.abort(); + } + }, - verticalDirection = this.dragDirection.vertical; - horizontalDirection = this.dragDirection.horizontal; - - return this.floating ? - ( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) : - ( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) ); + _isEventTargetInWidget: function( event ) { + var menuElement = this.menu.element[ 0 ]; + return event.target === this.element[ 0 ] || + event.target === menuElement || + $.contains( menuElement, event.target ); }, - _intersectsWithSides: function( item ) { + _closeOnClickOutside: function( event ) { + if ( !this._isEventTargetInWidget( event ) ) { + this.close(); + } + }, - var isOverBottomHalf = this._isOverAxis( this.positionAbs.top + - this.offset.click.top, item.top + ( item.height / 2 ), item.height ), - isOverRightHalf = this._isOverAxis( this.positionAbs.left + - this.offset.click.left, item.left + ( item.width / 2 ), item.width ), - verticalDirection = this.dragDirection.vertical, - horizontalDirection = this.dragDirection.horizontal; + _appendTo: function() { + var element = this.options.appendTo; - if ( this.floating && horizontalDirection ) { - return ( ( horizontalDirection === "right" && isOverRightHalf ) || - ( horizontalDirection === "left" && !isOverRightHalf ) ); - } else { - return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) || - ( verticalDirection === "up" && !isOverBottomHalf ) ); + if ( element ) { + element = element.jquery || element.nodeType ? + $( element ) : + this.document.find( element ).eq( 0 ); } - }, + if ( !element || !element[ 0 ] ) { + element = this.element.closest( ".ui-front, dialog" ); + } - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && ( delta > 0 ? "down" : "up" ); - }, + if ( !element.length ) { + element = this.document[ 0 ].body; + } - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && ( delta > 0 ? "right" : "left" ); + return element; }, - refresh: function( event ) { - this._refreshItems( event ); - this._setHandleClassName(); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? - [ options.connectWith ] : - options.connectWith; + _initSource: function() { + var array, url, + that = this; + if ( Array.isArray( this.options.source ) ) { + array = this.options.source; + this.source = function( request, response ) { + response( $.ui.autocomplete.filter( array, request.term ) ); + }; + } else if ( typeof this.options.source === "string" ) { + url = this.options.source; + this.source = function( request, response ) { + if ( that.xhr ) { + that.xhr.abort(); + } + that.xhr = $.ajax( { + url: url, + data: request, + dataType: "json", + success: function( data ) { + response( data ); + }, + error: function() { + response( [] ); + } + } ); + }; + } else { + this.source = this.options.source; + } }, - _getItemsAsjQuery: function( connected ) { + _searchTimeout: function( event ) { + clearTimeout( this.searching ); + this.searching = this._delay( function() { - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); + // Search if the value has changed, or if the user retypes the same value (see #7434) + var equalValues = this.term === this._value(), + menuVisible = this.menu.element.is( ":visible" ), + modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; - if ( connectWith && connected ) { - for ( i = connectWith.length - 1; i >= 0; i-- ) { - cur = $( connectWith[ i ], this.document[ 0 ] ); - for ( j = cur.length - 1; j >= 0; j-- ) { - inst = $.data( cur[ j ], this.widgetFullName ); - if ( inst && inst !== this && !inst.options.disabled ) { - queries.push( [ typeof inst.options.items === "function" ? - inst.options.items.call( inst.element ) : - $( inst.options.items, inst.element ) - .not( ".ui-sortable-helper" ) - .not( ".ui-sortable-placeholder" ), inst ] ); - } - } + if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) { + this.selectedItem = null; + this.search( null, event ); } - } + }, this.options.delay ); + }, - queries.push( [ typeof this.options.items === "function" ? - this.options.items - .call( this.element, null, { options: this.options, item: this.currentItem } ) : - $( this.options.items, this.element ) - .not( ".ui-sortable-helper" ) - .not( ".ui-sortable-placeholder" ), this ] ); + search: function( value, event ) { + value = value != null ? value : this._value(); - function addItems() { - items.push( this ); - } - for ( i = queries.length - 1; i >= 0; i-- ) { - queries[ i ][ 0 ].each( addItems ); + // Always save the actual value, not the one passed as an argument + this.term = this._value(); + + if ( value.length < this.options.minLength ) { + return this.close( event ); } - return $( items ); + if ( this._trigger( "search", event ) === false ) { + return; + } + return this._search( value ); }, - _removeCurrentsFromItems: function() { + _search: function( value ) { + this.pending++; + this._addClass( "ui-autocomplete-loading" ); + this.cancelSearch = false; - var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" ); + this.source( { term: value }, this._response() ); + }, - this.items = $.grep( this.items, function( item ) { - for ( var j = 0; j < list.length; j++ ) { - if ( list[ j ] === item.item[ 0 ] ) { - return false; - } + _response: function() { + var index = ++this.requestIndex; + + return function( content ) { + if ( index === this.requestIndex ) { + this.__response( content ); } - return true; - } ); + this.pending--; + if ( !this.pending ) { + this._removeClass( "ui-autocomplete-loading" ); + } + }.bind( this ); }, - _refreshItems: function( event ) { - - this.items = []; - this.containers = [ this ]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [ [ typeof this.options.items === "function" ? - this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) : - $( this.options.items, this.element ), this ] ], - connectWith = this._connectWith(); + __response: function( content ) { + if ( content ) { + content = this._normalize( content ); + } + this._trigger( "response", null, { content: content } ); + if ( !this.options.disabled && content && content.length && !this.cancelSearch ) { + this._suggest( content ); + this._trigger( "open" ); + } else { - //Shouldn't be run the first time through due to massive slow-down - if ( connectWith && this.ready ) { - for ( i = connectWith.length - 1; i >= 0; i-- ) { - cur = $( connectWith[ i ], this.document[ 0 ] ); - for ( j = cur.length - 1; j >= 0; j-- ) { - inst = $.data( cur[ j ], this.widgetFullName ); - if ( inst && inst !== this && !inst.options.disabled ) { - queries.push( [ typeof inst.options.items === "function" ? - inst.options.items - .call( inst.element[ 0 ], event, { item: this.currentItem } ) : - $( inst.options.items, inst.element ), inst ] ); - this.containers.push( inst ); - } - } - } + // use ._close() instead of .close() so we don't cancel future searches + this._close(); } + }, - for ( i = queries.length - 1; i >= 0; i-- ) { - targetData = queries[ i ][ 1 ]; - _queries = queries[ i ][ 0 ]; + close: function( event ) { + this.cancelSearch = true; + this._close( event ); + }, - for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) { - item = $( _queries[ j ] ); + _close: function( event ) { - // Data for target checking (mouse manager) - item.data( this.widgetName + "-item", targetData ); + // Remove the handler that closes the menu on outside clicks + this._off( this.document, "mousedown" ); - items.push( { - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - } ); - } + if ( this.menu.element.is( ":visible" ) ) { + this.menu.element.hide(); + this.menu.blur(); + this.isNewMenu = true; + this._trigger( "close", event ); } - }, - _refreshItemPositions: function( fast ) { - var i, item, t, p; + _change: function( event ) { + if ( this.previous !== this._value() ) { + this._trigger( "change", event, { item: this.selectedItem } ); + } + }, - for ( i = this.items.length - 1; i >= 0; i-- ) { - item = this.items[ i ]; + _normalize: function( items ) { - //We ignore calculating positions of all connected containers when we're not over them - if ( this.currentContainer && item.instance !== this.currentContainer && - item.item[ 0 ] !== this.currentItem[ 0 ] ) { - continue; + // assume all items have the right format when the first item is complete + if ( items.length && items[ 0 ].label && items[ 0 ].value ) { + return items; + } + return $.map( items, function( item ) { + if ( typeof item === "string" ) { + return { + label: item, + value: item + }; } + return $.extend( {}, item, { + label: item.label || item.value, + value: item.value || item.label + } ); + } ); + }, - t = this.options.toleranceElement ? - $( this.options.toleranceElement, item.item ) : - item.item; + _suggest: function( items ) { + var ul = this.menu.element.empty(); + this._renderMenu( ul, items ); + this.isNewMenu = true; + this.menu.refresh(); - if ( !fast ) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } + // Size and position menu + ul.show(); + this._resizeMenu(); + ul.position( $.extend( { + of: this.element + }, this.options.position ) ); - p = t.offset(); - item.left = p.left; - item.top = p.top; + if ( this.options.autoFocus ) { + this.menu.next(); } + + // Listen for interactions outside of the widget (#6642) + this._on( this.document, { + mousedown: "_closeOnClickOutside" + } ); }, - refreshPositions: function( fast ) { + _resizeMenu: function() { + var ul = this.menu.element; + ul.outerWidth( Math.max( - // Determine whether items are being displayed horizontally - this.floating = this.items.length ? - this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) : - false; + // Firefox wraps long text (possibly a rounding bug) + // so we add 1px to avoid the wrapping (#7513) + ul.width( "" ).outerWidth() + 1, + this.element.outerWidth() + ) ); + }, - // This has to be redone because due to the item being moved out/into the offsetParent, - // the offsetParent's position will change - if ( this.offsetParent && this.helper ) { - this.offset.parent = this._getParentOffset(); - } - - this._refreshItemPositions( fast ); - - var i, p; - - if ( this.options.custom && this.options.custom.refreshContainers ) { - this.options.custom.refreshContainers.call( this ); - } else { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - p = this.containers[ i ].element.offset(); - this.containers[ i ].containerCache.left = p.left; - this.containers[ i ].containerCache.top = p.top; - this.containers[ i ].containerCache.width = - this.containers[ i ].element.outerWidth(); - this.containers[ i ].containerCache.height = - this.containers[ i ].element.outerHeight(); - } - } + _renderMenu: function( ul, items ) { + var that = this; + $.each( items, function( index, item ) { + that._renderItemData( ul, item ); + } ); + }, - return this; + _renderItemData: function( ul, item ) { + return this._renderItem( ul, item ).data( "ui-autocomplete-item", item ); }, - _createPlaceholder: function( that ) { - that = that || this; - var className, nodeName, - o = that.options; + _renderItem: function( ul, item ) { + return $( "<li>" ) + .append( $( "<div>" ).text( item.label ) ) + .appendTo( ul ); + }, - if ( !o.placeholder || o.placeholder.constructor === String ) { - className = o.placeholder; - nodeName = that.currentItem[ 0 ].nodeName.toLowerCase(); - o.placeholder = { - element: function() { + _move: function( direction, event ) { + if ( !this.menu.element.is( ":visible" ) ) { + this.search( null, event ); + return; + } + if ( this.menu.isFirstItem() && /^previous/.test( direction ) || + this.menu.isLastItem() && /^next/.test( direction ) ) { - var element = $( "<" + nodeName + ">", that.document[ 0 ] ); + if ( !this.isMultiLine ) { + this._value( this.term ); + } - that._addClass( element, "ui-sortable-placeholder", - className || that.currentItem[ 0 ].className ) - ._removeClass( element, "ui-sortable-helper" ); + this.menu.blur(); + return; + } + this.menu[ direction ]( event ); + }, - if ( nodeName === "tbody" ) { - that._createTrPlaceholder( - that.currentItem.find( "tr" ).eq( 0 ), - $( "<tr>", that.document[ 0 ] ).appendTo( element ) - ); - } else if ( nodeName === "tr" ) { - that._createTrPlaceholder( that.currentItem, element ); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } + widget: function() { + return this.menu.element; + }, - if ( !className ) { - element.css( "visibility", "hidden" ); - } + _value: function() { + return this.valueMethod.apply( this.element, arguments ); + }, - return element; - }, - update: function( container, p ) { + _keyEvent: function( keyEvent, event ) { + if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { + this._move( keyEvent, event ); - // 1. If a className is set as 'placeholder option, we don't force sizes - - // the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a - // class name is specified - if ( className && !o.forcePlaceholderSize ) { - return; - } + // Prevents moving cursor to beginning/end of the text field in some browsers + event.preventDefault(); + } + }, - // If the element doesn't have a actual height or width by itself (without - // styles coming from a stylesheet), it receives the inline height and width - // from the dragged item. Or, if it's a tbody or tr, it's going to have a height - // anyway since we're populating them with <td>s above, but they're unlikely to - // be the correct height on their own if the row heights are dynamic, so we'll - // always assign the height of the dragged item given forcePlaceholderSize - // is true. - if ( !p.height() || ( o.forcePlaceholderSize && - ( nodeName === "tbody" || nodeName === "tr" ) ) ) { - p.height( - that.currentItem.innerHeight() - - parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) - - parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) ); - } - if ( !p.width() ) { - p.width( - that.currentItem.innerWidth() - - parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) - - parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) ); - } - } - }; + // Support: Chrome <=50 + // We should be able to just use this.element.prop( "isContentEditable" ) + // but hidden elements always report false in Chrome. + // https://code.google.com/p/chromium/issues/detail?id=313082 + _isContentEditable: function( element ) { + if ( !element.length ) { + return false; } - //Create the placeholder - that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) ); + var editable = element.prop( "contentEditable" ); - //Append it after the actual current item - that.currentItem.after( that.placeholder ); + if ( editable === "inherit" ) { + return this._isContentEditable( element.parent() ); + } - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update( that, that.placeholder ); + return editable === "true"; + } +} ); +$.extend( $.ui.autocomplete, { + escapeRegex: function( value ) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); }, - - _createTrPlaceholder: function( sourceTr, targetTr ) { - var that = this; - - sourceTr.children().each( function() { - $( "<td> </td>", that.document[ 0 ] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( targetTr ); + filter: function( array, term ) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" ); + return $.grep( array, function( value ) { + return matcher.test( value.label || value.value || value ); } ); - }, + } +} ); - _contactContainers: function( event ) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom, - floating, axis, - innermostContainer = null, - innermostIndex = null; +// Live region extension, adding a `messages` option +// NOTE: This is an experimental API. We are still investigating +// a full solution for string manipulation and internationalization. +$.widget( "ui.autocomplete", $.ui.autocomplete, { + options: { + messages: { + noResults: "No search results.", + results: function( amount ) { + return amount + ( amount > 1 ? " results are" : " result is" ) + + " available, use up and down arrow keys to navigate."; + } + } + }, - // Get innermost container that intersects with item - for ( i = this.containers.length - 1; i >= 0; i-- ) { + __response: function( content ) { + var message; + this._superApply( arguments ); + if ( this.options.disabled || this.cancelSearch ) { + return; + } + if ( content && content.length ) { + message = this.options.messages.results( content.length ); + } else { + message = this.options.messages.noResults; + } + clearTimeout( this.liveRegionTimer ); + this.liveRegionTimer = this._delay( function() { + this.liveRegion.html( $( "<div>" ).text( message ) ); + }, 100 ); + } +} ); - // Never consider a container that's located within the item itself - if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) { - continue; - } +var widgetsAutocomplete = $.ui.autocomplete; - if ( this._intersectsWith( this.containers[ i ].containerCache ) ) { - // If we've already found a container and it's more "inner" than this, then continue - if ( innermostContainer && - $.contains( - this.containers[ i ].element[ 0 ], - innermostContainer.element[ 0 ] ) ) { - continue; - } +/*! + * jQuery UI Controlgroup 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - innermostContainer = this.containers[ i ]; - innermostIndex = i; +//>>label: Controlgroup +//>>group: Widgets +//>>description: Visually groups form control widgets +//>>docs: http://api.jqueryui.com/controlgroup/ +//>>demos: http://jqueryui.com/controlgroup/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/controlgroup.css +//>>css.theme: ../../themes/base/theme.css - } else { - // container doesn't intersect. trigger "out" event if necessary - if ( this.containers[ i ].containerCache.over ) { - this.containers[ i ]._trigger( "out", event, this._uiHash( this ) ); - this.containers[ i ].containerCache.over = 0; - } - } +var controlgroupCornerRegex = /ui-corner-([a-z]){2,6}/g; +var widgetsControlgroup = $.widget( "ui.controlgroup", { + version: "1.13.2", + defaultElement: "<div>", + options: { + direction: "horizontal", + disabled: null, + onlyVisible: true, + items: { + "button": "input[type=button], input[type=submit], input[type=reset], button, a", + "controlgroupLabel": ".ui-controlgroup-label", + "checkboxradio": "input[type='checkbox'], input[type='radio']", + "selectmenu": "select", + "spinner": ".ui-spinner-input" } + }, - // If no intersecting containers found, return - if ( !innermostContainer ) { - return; - } + _create: function() { + this._enhance(); + }, - // Move the item into the container if it's not there already - if ( this.containers.length === 1 ) { - if ( !this.containers[ innermostIndex ].containerCache.over ) { - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); - this.containers[ innermostIndex ].containerCache.over = 1; - } - } else { + // To support the enhanced option in jQuery Mobile, we isolate DOM manipulation + _enhance: function() { + this.element.attr( "role", "toolbar" ); + this.refresh(); + }, - // When entering a new container, we will find the item with the least distance and - // append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || this._isFloating( this.currentItem ); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - axis = floating ? "pageX" : "pageY"; + _destroy: function() { + this._callChildMethod( "destroy" ); + this.childWidgets.removeData( "ui-controlgroup-data" ); + this.element.removeAttr( "role" ); + if ( this.options.items.controlgroupLabel ) { + this.element + .find( this.options.items.controlgroupLabel ) + .find( ".ui-controlgroup-label-contents" ) + .contents().unwrap(); + } + }, - for ( j = this.items.length - 1; j >= 0; j-- ) { - if ( !$.contains( - this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] ) - ) { - continue; - } - if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) { - continue; - } + _initWidgets: function() { + var that = this, + childWidgets = []; - cur = this.items[ j ].item.offset()[ posProperty ]; - nearBottom = false; - if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) { - nearBottom = true; - } + // First we iterate over each of the items options + $.each( this.options.items, function( widget, selector ) { + var labels; + var options = {}; - if ( Math.abs( event[ axis ] - cur ) < dist ) { - dist = Math.abs( event[ axis ] - cur ); - itemWithLeastDistance = this.items[ j ]; - this.direction = nearBottom ? "up" : "down"; - } + // Make sure the widget has a selector set + if ( !selector ) { + return; } - //Check if dropOnEmpty is enabled - if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) { + if ( widget === "controlgroupLabel" ) { + labels = that.element.find( selector ); + labels.each( function() { + var element = $( this ); + + if ( element.children( ".ui-controlgroup-label-contents" ).length ) { + return; + } + element.contents() + .wrapAll( "<span class='ui-controlgroup-label-contents'></span>" ); + } ); + that._addClass( labels, null, "ui-widget ui-widget-content ui-state-default" ); + childWidgets = childWidgets.concat( labels.get() ); return; } - if ( this.currentContainer === this.containers[ innermostIndex ] ) { - if ( !this.currentContainer.containerCache.over ) { - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() ); - this.currentContainer.containerCache.over = 1; - } + // Make sure the widget actually exists + if ( !$.fn[ widget ] ) { return; } - if ( itemWithLeastDistance ) { - this._rearrange( event, itemWithLeastDistance, null, true ); + // We assume everything is in the middle to start because we can't determine + // first / last elements until all enhancments are done. + if ( that[ "_" + widget + "Options" ] ) { + options = that[ "_" + widget + "Options" ]( "middle" ); } else { - this._rearrange( event, null, this.containers[ innermostIndex ].element, true ); - } - this._trigger( "change", event, this._uiHash() ); - this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) ); - this.currentContainer = this.containers[ innermostIndex ]; - - //Update the placeholder - this.options.placeholder.update( this.currentContainer, this.placeholder ); - - //Update scrollParent - this.scrollParent = this.placeholder.scrollParent(); - - //Update overflowOffset - if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ].tagName !== "HTML" ) { - this.overflowOffset = this.scrollParent.offset(); + options = { classes: {} }; } - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); - this.containers[ innermostIndex ].containerCache.over = 1; - } - - }, + // Find instances of this widget inside controlgroup and init them + that.element + .find( selector ) + .each( function() { + var element = $( this ); + var instance = element[ widget ]( "instance" ); - _createHelper: function( event ) { + // We need to clone the default options for this type of widget to avoid + // polluting the variable options which has a wider scope than a single widget. + var instanceOptions = $.widget.extend( {}, options ); - var o = this.options, - helper = typeof o.helper === "function" ? - $( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) : - ( o.helper === "clone" ? this.currentItem.clone() : this.currentItem ); + // If the button is the child of a spinner ignore it + // TODO: Find a more generic solution + if ( widget === "button" && element.parent( ".ui-spinner" ).length ) { + return; + } - //Add the helper to the DOM if that didn't happen already - if ( !helper.parents( "body" ).length ) { - this.appendTo[ 0 ].appendChild( helper[ 0 ] ); - } + // Create the widget if it doesn't exist + if ( !instance ) { + instance = element[ widget ]()[ widget ]( "instance" ); + } + if ( instance ) { + instanceOptions.classes = + that._resolveClassesValues( instanceOptions.classes, instance ); + } + element[ widget ]( instanceOptions ); - if ( helper[ 0 ] === this.currentItem[ 0 ] ) { - this._storedCSS = { - width: this.currentItem[ 0 ].style.width, - height: this.currentItem[ 0 ].style.height, - position: this.currentItem.css( "position" ), - top: this.currentItem.css( "top" ), - left: this.currentItem.css( "left" ) - }; - } + // Store an instance of the controlgroup to be able to reference + // from the outermost element for changing options and refresh + var widgetElement = element[ widget ]( "widget" ); + $.data( widgetElement[ 0 ], "ui-controlgroup-data", + instance ? instance : element[ widget ]( "instance" ) ); - if ( !helper[ 0 ].style.width || o.forceHelperSize ) { - helper.width( this.currentItem.width() ); - } - if ( !helper[ 0 ].style.height || o.forceHelperSize ) { - helper.height( this.currentItem.height() ); - } + childWidgets.push( widgetElement[ 0 ] ); + } ); + } ); - return helper; + this.childWidgets = $( $.uniqueSort( childWidgets ) ); + this._addClass( this.childWidgets, "ui-controlgroup-item" ); + }, + _callChildMethod: function( method ) { + this.childWidgets.each( function() { + var element = $( this ), + data = element.data( "ui-controlgroup-data" ); + if ( data && data[ method ] ) { + data[ method ](); + } + } ); }, - _adjustOffsetFromHelper: function( obj ) { - if ( typeof obj === "string" ) { - obj = obj.split( " " ); - } - if ( Array.isArray( obj ) ) { - obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; - } - if ( "left" in obj ) { - this.offset.click.left = obj.left + this.margins.left; - } - if ( "right" in obj ) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ( "top" in obj ) { - this.offset.click.top = obj.top + this.margins.top; - } - if ( "bottom" in obj ) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } + _updateCornerClass: function( element, position ) { + var remove = "ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-corner-all"; + var add = this._buildSimpleOptions( position, "label" ).classes.label; + + this._removeClass( element, null, remove ); + this._addClass( element, null, add ); }, - _getParentOffset: function() { + _buildSimpleOptions: function( position, key ) { + var direction = this.options.direction === "vertical"; + var result = { + classes: {} + }; + result.classes[ key ] = { + "middle": "", + "first": "ui-corner-" + ( direction ? "top" : "left" ), + "last": "ui-corner-" + ( direction ? "bottom" : "right" ), + "only": "ui-corner-all" + }[ position ]; - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); + return result; + }, - // This is a special case where we need to modify a offset calculated on start, since the - // following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the - // next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't - // the document, which means that the scroll is included in the initial calculation of the - // offset of the parent, and never recalculated upon drag - if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } + _spinnerOptions: function( position ) { + var options = this._buildSimpleOptions( position, "ui-spinner" ); - // This needs to be actually done for all browsers, since pageX/pageY includes this - // information with an ugly IE fix - if ( this.offsetParent[ 0 ] === this.document[ 0 ].body || - ( this.offsetParent[ 0 ].tagName && - this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) { - po = { top: 0, left: 0 }; - } + options.classes[ "ui-spinner-up" ] = ""; + options.classes[ "ui-spinner-down" ] = ""; - return { - top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), - left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) - }; + return options; + }, + _buttonOptions: function( position ) { + return this._buildSimpleOptions( position, "ui-button" ); }, - _getRelativeOffset: function() { + _checkboxradioOptions: function( position ) { + return this._buildSimpleOptions( position, "ui-checkboxradio-label" ); + }, - if ( this.cssPosition === "relative" ) { - var p = this.currentItem.position(); - return { - top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + - this.scrollParent.scrollTop(), - left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + - this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } + _selectmenuOptions: function( position ) { + var direction = this.options.direction === "vertical"; + return { + width: direction ? "auto" : false, + classes: { + middle: { + "ui-selectmenu-button-open": "", + "ui-selectmenu-button-closed": "" + }, + first: { + "ui-selectmenu-button-open": "ui-corner-" + ( direction ? "top" : "tl" ), + "ui-selectmenu-button-closed": "ui-corner-" + ( direction ? "top" : "left" ) + }, + last: { + "ui-selectmenu-button-open": direction ? "" : "ui-corner-tr", + "ui-selectmenu-button-closed": "ui-corner-" + ( direction ? "bottom" : "right" ) + }, + only: { + "ui-selectmenu-button-open": "ui-corner-top", + "ui-selectmenu-button-closed": "ui-corner-all" + } + }[ position ] + }; }, - _cacheMargins: function() { - this.margins = { - left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ), - top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 ) - }; + _resolveClassesValues: function( classes, instance ) { + var result = {}; + $.each( classes, function( key ) { + var current = instance.options.classes[ key ] || ""; + current = String.prototype.trim.call( current.replace( controlgroupCornerRegex, "" ) ); + result[ key ] = ( current + " " + classes[ key ] ).replace( /\s+/g, " " ); + } ); + return result; }, - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; + _setOption: function( key, value ) { + if ( key === "direction" ) { + this._removeClass( "ui-controlgroup-" + this.options.direction ); + } + + this._super( key, value ); + if ( key === "disabled" ) { + this._callChildMethod( value ? "disable" : "enable" ); + return; + } + + this.refresh(); }, - _setContainment: function() { + refresh: function() { + var children, + that = this; - var ce, co, over, - o = this.options; - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; + this._addClass( "ui-controlgroup ui-controlgroup-" + this.options.direction ); + + if ( this.options.direction === "horizontal" ) { + this._addClass( null, "ui-helper-clearfix" ); } - if ( o.containment === "document" || o.containment === "window" ) { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - o.containment === "document" ? - this.document.width() : - this.window.width() - this.helperProportions.width - this.margins.left, - ( o.containment === "document" ? - ( this.document.height() || document.body.parentNode.scrollHeight ) : - this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight - ) - this.helperProportions.height - this.margins.top - ]; + this._initWidgets(); + + children = this.childWidgets; + + // We filter here because we need to track all childWidgets not just the visible ones + if ( this.options.onlyVisible ) { + children = children.filter( ":visible" ); } - if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) { - ce = $( o.containment )[ 0 ]; - co = $( o.containment ).offset(); - over = ( $( ce ).css( "overflow" ) !== "hidden" ); + if ( children.length ) { - this.containment = [ - co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) + - ( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left, - co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) + - ( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top, - co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - - ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) - - ( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) - - this.helperProportions.width - this.margins.left, - co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - - ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) - - ( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) - - this.helperProportions.height - this.margins.top - ]; + // We do this last because we need to make sure all enhancment is done + // before determining first and last + $.each( [ "first", "last" ], function( index, value ) { + var instance = children[ value ]().data( "ui-controlgroup-data" ); + + if ( instance && that[ "_" + instance.widgetName + "Options" ] ) { + var options = that[ "_" + instance.widgetName + "Options" ]( + children.length === 1 ? "only" : value + ); + options.classes = that._resolveClassesValues( options.classes, instance ); + instance.element[ instance.widgetName ]( options ); + } else { + that._updateCornerClass( children[ value ](), value ); + } + } ); + + // Finally call the refresh method on each of the child widgets. + this._callChildMethod( "refresh" ); } + } +} ); - }, +/*! + * jQuery UI Checkboxradio 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - _convertPositionTo: function( d, pos ) { +//>>label: Checkboxradio +//>>group: Widgets +//>>description: Enhances a form with multiple themeable checkboxes or radio buttons. +//>>docs: http://api.jqueryui.com/checkboxradio/ +//>>demos: http://jqueryui.com/checkboxradio/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/button.css +//>>css.structure: ../../themes/base/checkboxradio.css +//>>css.theme: ../../themes/base/theme.css - if ( !pos ) { - pos = this.position; + +$.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { + version: "1.13.2", + options: { + disabled: null, + label: null, + icon: true, + classes: { + "ui-checkboxradio-label": "ui-corner-all", + "ui-checkboxradio-icon": "ui-corner-all" } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && - !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? - this.offsetParent : - this.scrollParent, - scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); + }, - return { - top: ( + _getCreateOptions: function() { + var disabled, labels, labelContents; + var options = this._super() || {}; - // The absolute mouse position - pos.top + + // We read the type here, because it makes more sense to throw a element type error first, + // rather then the error for lack of a label. Often if its the wrong type, it + // won't have a label (e.g. calling on a div, btn, etc) + this._readType(); - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top * mod + + labels = this.element.labels(); - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top * mod - - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollTop() : - ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod ) - ), - left: ( + // If there are multiple labels, use the last one + this.label = $( labels[ labels.length - 1 ] ); + if ( !this.label.length ) { + $.error( "No label found for checkboxradio widget" ); + } - // The absolute mouse position - pos.left + + this.originalLabel = ""; - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left * mod + + // We need to get the label text but this may also need to make sure it does not contain the + // input itself. + // The label contents could be text, html, or a mix. We wrap all elements + // and read the wrapper's `innerHTML` to get a string representation of + // the label, without the input as part of it. + labelContents = this.label.contents().not( this.element[ 0 ] ); - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left * mod - - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : - scroll.scrollLeft() ) * mod ) - ) - }; + if ( labelContents.length ) { + this.originalLabel += labelContents + .clone() + .wrapAll( "<div></div>" ) + .parent() + .html(); + } + + // Set the label option if we found label text + if ( this.originalLabel ) { + options.label = this.originalLabel; + } + disabled = this.element[ 0 ].disabled; + if ( disabled != null ) { + options.disabled = disabled; + } + return options; }, - _generatePosition: function( event ) { + _create: function() { + var checked = this.element[ 0 ].checked; - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && - !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? - this.offsetParent : - this.scrollParent, - scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); + this._bindFormResetHandler(); - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) { - this.offset.relative = this._getRelativeOffset(); + if ( this.options.disabled == null ) { + this.options.disabled = this.element[ 0 ].disabled; } - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ + this._setOption( "disabled", this.options.disabled ); + this._addClass( "ui-checkboxradio", "ui-helper-hidden-accessible" ); + this._addClass( this.label, "ui-checkboxradio-label", "ui-button ui-widget" ); - if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options + if ( this.type === "radio" ) { + this._addClass( this.label, "ui-checkboxradio-radio-label" ); + } - if ( this.containment ) { - if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) { - pageX = this.containment[ 0 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) { - pageY = this.containment[ 1 ] + this.offset.click.top; - } - if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) { - pageX = this.containment[ 2 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) { - pageY = this.containment[ 3 ] + this.offset.click.top; - } - } - - if ( o.grid ) { - top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) / - o.grid[ 1 ] ) * o.grid[ 1 ]; - pageY = this.containment ? - ( ( top - this.offset.click.top >= this.containment[ 1 ] && - top - this.offset.click.top <= this.containment[ 3 ] ) ? - top : - ( ( top - this.offset.click.top >= this.containment[ 1 ] ) ? - top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : - top; + if ( this.options.label && this.options.label !== this.originalLabel ) { + this._updateLabel(); + } else if ( this.originalLabel ) { + this.options.label = this.originalLabel; + } - left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) / - o.grid[ 0 ] ) * o.grid[ 0 ]; - pageX = this.containment ? - ( ( left - this.offset.click.left >= this.containment[ 0 ] && - left - this.offset.click.left <= this.containment[ 2 ] ) ? - left : - ( ( left - this.offset.click.left >= this.containment[ 0 ] ) ? - left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : - left; - } + this._enhance(); + if ( checked ) { + this._addClass( this.label, "ui-checkboxradio-checked", "ui-state-active" ); } - return { - top: ( - - // The absolute mouse position - pageY - + this._on( { + change: "_toggleClasses", + focus: function() { + this._addClass( this.label, null, "ui-state-focus ui-visual-focus" ); + }, + blur: function() { + this._removeClass( this.label, null, "ui-state-focus ui-visual-focus" ); + } + } ); + }, - // Click offset (relative to the element) - this.offset.click.top - + _readType: function() { + var nodeName = this.element[ 0 ].nodeName.toLowerCase(); + this.type = this.element[ 0 ].type; + if ( nodeName !== "input" || !/radio|checkbox/.test( this.type ) ) { + $.error( "Can't create checkboxradio on element.nodeName=" + nodeName + + " and element.type=" + this.type ); + } + }, - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top - + // Support jQuery Mobile enhanced option + _enhance: function() { + this._updateIcon( this.element[ 0 ].checked ); + }, - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top + - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollTop() : - ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) ) - ), - left: ( + widget: function() { + return this.label; + }, - // The absolute mouse position - pageX - + _getRadioGroup: function() { + var group; + var name = this.element[ 0 ].name; + var nameSelector = "input[name='" + $.escapeSelector( name ) + "']"; - // Click offset (relative to the element) - this.offset.click.left - + if ( !name ) { + return $( [] ); + } - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left - + if ( this.form.length ) { + group = $( this.form[ 0 ].elements ).filter( nameSelector ); + } else { - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left + - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollLeft() : - scrollIsRootNode ? 0 : scroll.scrollLeft() ) ) - ) - }; + // Not inside a form, check all inputs that also are not inside a form + group = $( nameSelector ).filter( function() { + return $( this )._form().length === 0; + } ); + } + return group.not( this.element ); }, - _rearrange: function( event, i, a, hardRefresh ) { + _toggleClasses: function() { + var checked = this.element[ 0 ].checked; + this._toggleClass( this.label, "ui-checkboxradio-checked", "ui-state-active", checked ); - if ( a ) { - a[ 0 ].appendChild( this.placeholder[ 0 ] ); - } else { - i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ], - ( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) ); + if ( this.options.icon && this.type === "checkbox" ) { + this._toggleClass( this.icon, null, "ui-icon-check ui-state-checked", checked ) + ._toggleClass( this.icon, null, "ui-icon-blank", !checked ); } - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, - // if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay( function() { - if ( counter === this.counter ) { - - //Precompute after each DOM insertion, NOT on mousemove - this.refreshPositions( !hardRefresh ); - } - } ); + if ( this.type === "radio" ) { + this._getRadioGroup() + .each( function() { + var instance = $( this ).checkboxradio( "instance" ); + if ( instance ) { + instance._removeClass( instance.label, + "ui-checkboxradio-checked", "ui-state-active" ); + } + } ); + } }, - _clear: function( event, noPropagation ) { + _destroy: function() { + this._unbindFormResetHandler(); - this.reverting = false; + if ( this.icon ) { + this.icon.remove(); + this.iconSpace.remove(); + } + }, - // We delay all events that have to be triggered to after the point where the placeholder - // has been removed and everything else normalized again - var i, - delayedTriggers = []; + _setOption: function( key, value ) { - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets - // reappended (see #4088) - if ( !this._noFinalSort && this.currentItem.parent().length ) { - this.placeholder.before( this.currentItem ); + // We don't allow the value to be set to nothing + if ( key === "label" && !value ) { + return; } - this._noFinalSort = null; - if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) { - for ( i in this._storedCSS ) { - if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) { - this._storedCSS[ i ] = ""; - } - } - this.currentItem.css( this._storedCSS ); - this._removeClass( this.currentItem, "ui-sortable-helper" ); - } else { - this.currentItem.show(); - } + this._super( key, value ); - if ( this.fromOutside && !noPropagation ) { - delayedTriggers.push( function( event ) { - this._trigger( "receive", event, this._uiHash( this.fromOutside ) ); - } ); - } - if ( ( this.fromOutside || - this.domPosition.prev !== - this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] || - this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) { + if ( key === "disabled" ) { + this._toggleClass( this.label, null, "ui-state-disabled", value ); + this.element[ 0 ].disabled = value; - // Trigger update callback if the DOM position has changed - delayedTriggers.push( function( event ) { - this._trigger( "update", event, this._uiHash() ); - } ); + // Don't refresh when setting disabled + return; } + this.refresh(); + }, - // Check if the items Container has Changed and trigger appropriate - // events. - if ( this !== this.currentContainer ) { - if ( !noPropagation ) { - delayedTriggers.push( function( event ) { - this._trigger( "remove", event, this._uiHash() ); - } ); - delayedTriggers.push( ( function( c ) { - return function( event ) { - c._trigger( "receive", event, this._uiHash( this ) ); - }; - } ).call( this, this.currentContainer ) ); - delayedTriggers.push( ( function( c ) { - return function( event ) { - c._trigger( "update", event, this._uiHash( this ) ); - }; - } ).call( this, this.currentContainer ) ); + _updateIcon: function( checked ) { + var toAdd = "ui-icon ui-icon-background "; + + if ( this.options.icon ) { + if ( !this.icon ) { + this.icon = $( "<span>" ); + this.iconSpace = $( "<span> </span>" ); + this._addClass( this.iconSpace, "ui-checkboxradio-icon-space" ); } - } - //Post events to containers - function delayEvent( type, instance, container ) { - return function( event ) { - container._trigger( type, event, instance._uiHash( instance ) ); - }; - } - for ( i = this.containers.length - 1; i >= 0; i-- ) { - if ( !noPropagation ) { - delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); + if ( this.type === "checkbox" ) { + toAdd += checked ? "ui-icon-check ui-state-checked" : "ui-icon-blank"; + this._removeClass( this.icon, null, checked ? "ui-icon-blank" : "ui-icon-check" ); + } else { + toAdd += "ui-icon-blank"; } - if ( this.containers[ i ].containerCache.over ) { - delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); - this.containers[ i ].containerCache.over = 0; + this._addClass( this.icon, "ui-checkboxradio-icon", toAdd ); + if ( !checked ) { + this._removeClass( this.icon, null, "ui-icon-check ui-state-checked" ); } + this.icon.prependTo( this.label ).after( this.iconSpace ); + } else if ( this.icon !== undefined ) { + this.icon.remove(); + this.iconSpace.remove(); + delete this.icon; } + }, - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if ( this._storedOpacity ) { - this.helper.css( "opacity", this._storedOpacity ); - } - if ( this._storedZIndex ) { - this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex ); - } - - this.dragging = false; - - if ( !noPropagation ) { - this._trigger( "beforeStop", event, this._uiHash() ); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, - // it unbinds ALL events from the original node! - this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); + _updateLabel: function() { - if ( !this.cancelHelperRemoval ) { - if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { - this.helper.remove(); - } - this.helper = null; + // Remove the contents of the label ( minus the icon, icon space, and input ) + var contents = this.label.contents().not( this.element[ 0 ] ); + if ( this.icon ) { + contents = contents.not( this.icon[ 0 ] ); } - - if ( !noPropagation ) { - for ( i = 0; i < delayedTriggers.length; i++ ) { - - // Trigger all delayed events - delayedTriggers[ i ].call( this, event ); - } - this._trigger( "stop", event, this._uiHash() ); + if ( this.iconSpace ) { + contents = contents.not( this.iconSpace[ 0 ] ); } + contents.remove(); - this.fromOutside = false; - return !this.cancelHelperRemoval; - + this.label.append( this.options.label ); }, - _trigger: function() { - if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) { - this.cancel(); + refresh: function() { + var checked = this.element[ 0 ].checked, + isDisabled = this.element[ 0 ].disabled; + + this._updateIcon( checked ); + this._toggleClass( this.label, "ui-checkboxradio-checked", "ui-state-active", checked ); + if ( this.options.label !== null ) { + this._updateLabel(); } - }, - _uiHash: function( _inst ) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $( [] ), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; + if ( isDisabled !== this.options.disabled ) { + this._setOptions( { "disabled": isDisabled } ); + } } -} ); +} ] ); + +var widgetsCheckboxradio = $.ui.checkboxradio; /*! - * jQuery UI Accordion 1.13.1 + * jQuery UI Button 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -6843,7638 +6883,7214 @@ var widgetsSortable = $.widget( "ui.sortable", $.ui.mouse, { * http://jquery.org/license */ -//>>label: Accordion +//>>label: Button //>>group: Widgets -/* eslint-disable max-len */ -//>>description: Displays collapsible content panels for presenting information in a limited amount of space. -/* eslint-enable max-len */ -//>>docs: http://api.jqueryui.com/accordion/ -//>>demos: http://jqueryui.com/accordion/ +//>>description: Enhances a form with themeable buttons. +//>>docs: http://api.jqueryui.com/button/ +//>>demos: http://jqueryui.com/button/ //>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/accordion.css +//>>css.structure: ../../themes/base/button.css //>>css.theme: ../../themes/base/theme.css -var widgetsAccordion = $.widget( "ui.accordion", { - version: "1.13.1", +$.widget( "ui.button", { + version: "1.13.2", + defaultElement: "<button>", options: { - active: 0, - animate: {}, classes: { - "ui-accordion-header": "ui-corner-top", - "ui-accordion-header-collapsed": "ui-corner-all", - "ui-accordion-content": "ui-corner-bottom" - }, - collapsible: false, - event: "click", - header: function( elem ) { - return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); - }, - heightStyle: "auto", - icons: { - activeHeader: "ui-icon-triangle-1-s", - header: "ui-icon-triangle-1-e" + "ui-button": "ui-corner-all" }, - - // Callbacks - activate: null, - beforeActivate: null - }, - - hideProps: { - borderTopWidth: "hide", - borderBottomWidth: "hide", - paddingTop: "hide", - paddingBottom: "hide", - height: "hide" + disabled: null, + icon: null, + iconPosition: "beginning", + label: null, + showLabel: true }, - showProps: { - borderTopWidth: "show", - borderBottomWidth: "show", - paddingTop: "show", - paddingBottom: "show", - height: "show" - }, + _getCreateOptions: function() { + var disabled, - _create: function() { - var options = this.options; + // This is to support cases like in jQuery Mobile where the base widget does have + // an implementation of _getCreateOptions + options = this._super() || {}; - this.prevShow = this.prevHide = $(); - this._addClass( "ui-accordion", "ui-widget ui-helper-reset" ); - this.element.attr( "role", "tablist" ); + this.isInput = this.element.is( "input" ); - // Don't allow collapsible: false and active: false / null - if ( !options.collapsible && ( options.active === false || options.active == null ) ) { - options.active = 0; + disabled = this.element[ 0 ].disabled; + if ( disabled != null ) { + options.disabled = disabled; } - this._processPanels(); - - // handle negative values - if ( options.active < 0 ) { - options.active += this.headers.length; + this.originalLabel = this.isInput ? this.element.val() : this.element.html(); + if ( this.originalLabel ) { + options.label = this.originalLabel; } - this._refresh(); - }, - _getCreateEventData: function() { - return { - header: this.active, - panel: !this.active.length ? $() : this.active.next() - }; + return options; }, - _createIcons: function() { - var icon, children, - icons = this.options.icons; - - if ( icons ) { - icon = $( "<span>" ); - this._addClass( icon, "ui-accordion-header-icon", "ui-icon " + icons.header ); - icon.prependTo( this.headers ); - children = this.active.children( ".ui-accordion-header-icon" ); - this._removeClass( children, icons.header ) - ._addClass( children, null, icons.activeHeader ) - ._addClass( this.headers, "ui-accordion-icons" ); + _create: function() { + if ( !this.option.showLabel & !this.options.icon ) { + this.options.showLabel = true; } - }, - _destroyIcons: function() { - this._removeClass( this.headers, "ui-accordion-icons" ); - this.headers.children( ".ui-accordion-header-icon" ).remove(); - }, + // We have to check the option again here even though we did in _getCreateOptions, + // because null may have been passed on init which would override what was set in + // _getCreateOptions + if ( this.options.disabled == null ) { + this.options.disabled = this.element[ 0 ].disabled || false; + } - _destroy: function() { - var contents; + this.hasTitle = !!this.element.attr( "title" ); - // Clean up main element - this.element.removeAttr( "role" ); + // Check to see if the label needs to be set or if its already correct + if ( this.options.label && this.options.label !== this.originalLabel ) { + if ( this.isInput ) { + this.element.val( this.options.label ); + } else { + this.element.html( this.options.label ); + } + } + this._addClass( "ui-button", "ui-widget" ); + this._setOption( "disabled", this.options.disabled ); + this._enhance(); - // Clean up headers - this.headers - .removeAttr( "role aria-expanded aria-selected aria-controls tabIndex" ) - .removeUniqueId(); + if ( this.element.is( "a" ) ) { + this._on( { + "keyup": function( event ) { + if ( event.keyCode === $.ui.keyCode.SPACE ) { + event.preventDefault(); - this._destroyIcons(); + // Support: PhantomJS <= 1.9, IE 8 Only + // If a native click is available use it so we actually cause navigation + // otherwise just trigger a click event + if ( this.element[ 0 ].click ) { + this.element[ 0 ].click(); + } else { + this.element.trigger( "click" ); + } + } + } + } ); + } + }, - // Clean up content panels - contents = this.headers.next() - .css( "display", "" ) - .removeAttr( "role aria-hidden aria-labelledby" ) - .removeUniqueId(); + _enhance: function() { + if ( !this.element.is( "button" ) ) { + this.element.attr( "role", "button" ); + } - if ( this.options.heightStyle !== "content" ) { - contents.css( "height", "" ); + if ( this.options.icon ) { + this._updateIcon( "icon", this.options.icon ); + this._updateTooltip(); } }, - _setOption: function( key, value ) { - if ( key === "active" ) { + _updateTooltip: function() { + this.title = this.element.attr( "title" ); - // _activate() will handle invalid values and update this.options - this._activate( value ); - return; + if ( !this.options.showLabel && !this.title ) { + this.element.attr( "title", this.options.label ); } + }, - if ( key === "event" ) { - if ( this.options.event ) { - this._off( this.headers, this.options.event ); - } - this._setupEvents( value ); - } + _updateIcon: function( option, value ) { + var icon = option !== "iconPosition", + position = icon ? this.options.iconPosition : value, + displayBlock = position === "top" || position === "bottom"; - this._super( key, value ); + // Create icon + if ( !this.icon ) { + this.icon = $( "<span>" ); - // Setting collapsible: false while collapsed; open first panel - if ( key === "collapsible" && !value && this.options.active === false ) { - this._activate( 0 ); - } + this._addClass( this.icon, "ui-button-icon", "ui-icon" ); - if ( key === "icons" ) { - this._destroyIcons(); - if ( value ) { - this._createIcons(); + if ( !this.options.showLabel ) { + this._addClass( "ui-button-icon-only" ); } + } else if ( icon ) { + + // If we are updating the icon remove the old icon class + this._removeClass( this.icon, null, this.options.icon ); } - }, - _setOptionDisabled: function( value ) { - this._super( value ); + // If we are updating the icon add the new icon class + if ( icon ) { + this._addClass( this.icon, null, value ); + } - this.element.attr( "aria-disabled", value ); + this._attachIcon( position ); - // Support: IE8 Only - // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE - // so we need to add the disabled class to the headers and panels - this._toggleClass( null, "ui-state-disabled", !!value ); - this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", - !!value ); - }, + // If the icon is on top or bottom we need to add the ui-widget-icon-block class and remove + // the iconSpace if there is one. + if ( displayBlock ) { + this._addClass( this.icon, null, "ui-widget-icon-block" ); + if ( this.iconSpace ) { + this.iconSpace.remove(); + } + } else { - _keydown: function( event ) { - if ( event.altKey || event.ctrlKey ) { - return; + // Position is beginning or end so remove the ui-widget-icon-block class and add the + // space if it does not exist + if ( !this.iconSpace ) { + this.iconSpace = $( "<span> </span>" ); + this._addClass( this.iconSpace, "ui-button-icon-space" ); + } + this._removeClass( this.icon, null, "ui-wiget-icon-block" ); + this._attachIconSpace( position ); } + }, - var keyCode = $.ui.keyCode, - length = this.headers.length, - currentIndex = this.headers.index( event.target ), - toFocus = false; + _destroy: function() { + this.element.removeAttr( "role" ); - switch ( event.keyCode ) { - case keyCode.RIGHT: - case keyCode.DOWN: - toFocus = this.headers[ ( currentIndex + 1 ) % length ]; - break; - case keyCode.LEFT: - case keyCode.UP: - toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; - break; - case keyCode.SPACE: - case keyCode.ENTER: - this._eventHandler( event ); - break; - case keyCode.HOME: - toFocus = this.headers[ 0 ]; - break; - case keyCode.END: - toFocus = this.headers[ length - 1 ]; - break; + if ( this.icon ) { + this.icon.remove(); } - - if ( toFocus ) { - $( event.target ).attr( "tabIndex", -1 ); - $( toFocus ).attr( "tabIndex", 0 ); - $( toFocus ).trigger( "focus" ); - event.preventDefault(); + if ( this.iconSpace ) { + this.iconSpace.remove(); } - }, - - _panelKeyDown: function( event ) { - if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { - $( event.currentTarget ).prev().trigger( "focus" ); + if ( !this.hasTitle ) { + this.element.removeAttr( "title" ); } }, - refresh: function() { - var options = this.options; - this._processPanels(); - - // Was collapsed or no panel - if ( ( options.active === false && options.collapsible === true ) || - !this.headers.length ) { - options.active = false; - this.active = $(); + _attachIconSpace: function( iconPosition ) { + this.icon[ /^(?:end|bottom)/.test( iconPosition ) ? "before" : "after" ]( this.iconSpace ); + }, - // active false only when collapsible is true - } else if ( options.active === false ) { - this._activate( 0 ); + _attachIcon: function( iconPosition ) { + this.element[ /^(?:end|bottom)/.test( iconPosition ) ? "append" : "prepend" ]( this.icon ); + }, - // was active, but active panel is gone - } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + _setOptions: function( options ) { + var newShowLabel = options.showLabel === undefined ? + this.options.showLabel : + options.showLabel, + newIcon = options.icon === undefined ? this.options.icon : options.icon; - // all remaining panel are disabled - if ( this.headers.length === this.headers.find( ".ui-state-disabled" ).length ) { - options.active = false; - this.active = $(); + if ( !newShowLabel && !newIcon ) { + options.showLabel = true; + } + this._super( options ); + }, - // activate previous panel - } else { - this._activate( Math.max( 0, options.active - 1 ) ); + _setOption: function( key, value ) { + if ( key === "icon" ) { + if ( value ) { + this._updateIcon( key, value ); + } else if ( this.icon ) { + this.icon.remove(); + if ( this.iconSpace ) { + this.iconSpace.remove(); + } } - - // was active, active panel still exists - } else { - - // make sure active index is correct - options.active = this.headers.index( this.active ); } - this._destroyIcons(); + if ( key === "iconPosition" ) { + this._updateIcon( key, value ); + } - this._refresh(); - }, + // Make sure we can't end up with a button that has neither text nor icon + if ( key === "showLabel" ) { + this._toggleClass( "ui-button-icon-only", null, !value ); + this._updateTooltip(); + } - _processPanels: function() { - var prevHeaders = this.headers, - prevPanels = this.panels; + if ( key === "label" ) { + if ( this.isInput ) { + this.element.val( value ); + } else { - if ( typeof this.options.header === "function" ) { - this.headers = this.options.header( this.element ); - } else { - this.headers = this.element.find( this.options.header ); + // If there is an icon, append it, else nothing then append the value + // this avoids removal of the icon when setting label text + this.element.html( value ); + if ( this.icon ) { + this._attachIcon( this.options.iconPosition ); + this._attachIconSpace( this.options.iconPosition ); + } + } } - this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", - "ui-state-default" ); - this.panels = this.headers.next().filter( ":not(.ui-accordion-content-active)" ).hide(); - this._addClass( this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content" ); + this._super( key, value ); - // Avoid memory leaks (#10056) - if ( prevPanels ) { - this._off( prevHeaders.not( this.headers ) ); - this._off( prevPanels.not( this.panels ) ); + if ( key === "disabled" ) { + this._toggleClass( null, "ui-state-disabled", value ); + this.element[ 0 ].disabled = value; + if ( value ) { + this.element.trigger( "blur" ); + } } }, - _refresh: function() { - var maxHeight, - options = this.options, - heightStyle = options.heightStyle, - parent = this.element.parent(); + refresh: function() { - this.active = this._findActive( options.active ); - this._addClass( this.active, "ui-accordion-header-active", "ui-state-active" ) - ._removeClass( this.active, "ui-accordion-header-collapsed" ); - this._addClass( this.active.next(), "ui-accordion-content-active" ); - this.active.next().show(); + // Make sure to only check disabled if its an element that supports this otherwise + // check for the disabled class to determine state + var isDisabled = this.element.is( "input, button" ) ? + this.element[ 0 ].disabled : this.element.hasClass( "ui-button-disabled" ); - this.headers - .attr( "role", "tab" ) - .each( function() { - var header = $( this ), - headerId = header.uniqueId().attr( "id" ), - panel = header.next(), - panelId = panel.uniqueId().attr( "id" ); - header.attr( "aria-controls", panelId ); - panel.attr( "aria-labelledby", headerId ); - } ) - .next() - .attr( "role", "tabpanel" ); + if ( isDisabled !== this.options.disabled ) { + this._setOptions( { disabled: isDisabled } ); + } - this.headers - .not( this.active ) - .attr( { - "aria-selected": "false", - "aria-expanded": "false", - tabIndex: -1 - } ) - .next() - .attr( { - "aria-hidden": "true" - } ) - .hide(); + this._updateTooltip(); + } +} ); - // Make sure at least one header is in the tab order - if ( !this.active.length ) { - this.headers.eq( 0 ).attr( "tabIndex", 0 ); - } else { - this.active.attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ) - .next() - .attr( { - "aria-hidden": "false" - } ); - } - - this._createIcons(); +// DEPRECATED +if ( $.uiBackCompat !== false ) { - this._setupEvents( options.event ); + // Text and Icons options + $.widget( "ui.button", $.ui.button, { + options: { + text: true, + icons: { + primary: null, + secondary: null + } + }, - if ( heightStyle === "fill" ) { - maxHeight = parent.height(); - this.element.siblings( ":visible" ).each( function() { - var elem = $( this ), - position = elem.css( "position" ); + _create: function() { + if ( this.options.showLabel && !this.options.text ) { + this.options.showLabel = this.options.text; + } + if ( !this.options.showLabel && this.options.text ) { + this.options.text = this.options.showLabel; + } + if ( !this.options.icon && ( this.options.icons.primary || + this.options.icons.secondary ) ) { + if ( this.options.icons.primary ) { + this.options.icon = this.options.icons.primary; + } else { + this.options.icon = this.options.icons.secondary; + this.options.iconPosition = "end"; + } + } else if ( this.options.icon ) { + this.options.icons.primary = this.options.icon; + } + this._super(); + }, - if ( position === "absolute" || position === "fixed" ) { - return; + _setOption: function( key, value ) { + if ( key === "text" ) { + this._super( "showLabel", value ); + return; + } + if ( key === "showLabel" ) { + this.options.text = value; + } + if ( key === "icon" ) { + this.options.icons.primary = value; + } + if ( key === "icons" ) { + if ( value.primary ) { + this._super( "icon", value.primary ); + this._super( "iconPosition", "beginning" ); + } else if ( value.secondary ) { + this._super( "icon", value.secondary ); + this._super( "iconPosition", "end" ); } - maxHeight -= elem.outerHeight( true ); - } ); + } + this._superApply( arguments ); + } + } ); - this.headers.each( function() { - maxHeight -= $( this ).outerHeight( true ); - } ); + $.fn.button = ( function( orig ) { + return function( options ) { + var isMethodCall = typeof options === "string"; + var args = Array.prototype.slice.call( arguments, 1 ); + var returnValue = this; - this.headers.next() - .each( function() { - $( this ).height( Math.max( 0, maxHeight - - $( this ).innerHeight() + $( this ).height() ) ); - } ) - .css( "overflow", "auto" ); - } else if ( heightStyle === "auto" ) { - maxHeight = 0; - this.headers.next() - .each( function() { - var isVisible = $( this ).is( ":visible" ); - if ( !isVisible ) { - $( this ).show(); - } - maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); - if ( !isVisible ) { - $( this ).hide(); - } - } ) - .height( maxHeight ); - } - }, + if ( isMethodCall ) { - _activate: function( index ) { - var active = this._findActive( index )[ 0 ]; + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var type = $( this ).attr( "type" ); + var name = type !== "checkbox" && type !== "radio" ? + "button" : + "checkboxradio"; + var instance = $.data( this, "ui-" + name ); - // Trying to activate the already active panel - if ( active === this.active[ 0 ] ) { - return; - } + if ( options === "instance" ) { + returnValue = instance; + return false; + } - // Trying to collapse, simulate a click on the currently active header - active = active || this.active[ 0 ]; + if ( !instance ) { + return $.error( "cannot call methods on button" + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } - this._eventHandler( { - target: active, - currentTarget: active, - preventDefault: $.noop - } ); - }, + if ( typeof instance[ options ] !== "function" || + options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for button" + + " widget instance" ); + } - _findActive: function( selector ) { - return typeof selector === "number" ? this.headers.eq( selector ) : $(); - }, + methodValue = instance[ options ].apply( instance, args ); - _setupEvents: function( event ) { - var events = { - keydown: "_keydown" - }; - if ( event ) { - $.each( event.split( " " ), function( index, eventName ) { - events[ eventName ] = "_eventHandler"; - } ); - } + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { - this._off( this.headers.add( this.headers.next() ) ); - this._on( this.headers, events ); - this._on( this.headers.next(), { keydown: "_panelKeyDown" } ); - this._hoverable( this.headers ); - this._focusable( this.headers ); - }, + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } - _eventHandler: function( event ) { - var activeChildren, clickedChildren, - options = this.options, - active = this.active, - clicked = $( event.currentTarget ), - clickedIsActive = clicked[ 0 ] === active[ 0 ], - collapsing = clickedIsActive && options.collapsible, - toShow = collapsing ? $() : clicked.next(), - toHide = active.next(), - eventData = { - oldHeader: active, - oldPanel: toHide, - newHeader: collapsing ? $() : clicked, - newPanel: toShow - }; + this.each( function() { + var type = $( this ).attr( "type" ); + var name = type !== "checkbox" && type !== "radio" ? "button" : "checkboxradio"; + var instance = $.data( this, "ui-" + name ); - event.preventDefault(); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + if ( name === "button" ) { + orig.call( $( this ), options ); + return; + } - if ( + $( this ).checkboxradio( $.extend( { icon: false }, options ) ); + } + } ); + } - // click on active header, but not collapsible - ( clickedIsActive && !options.collapsible ) || + return returnValue; + }; + } )( $.fn.button ); - // allow canceling activation - ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { - return; + $.fn.buttonset = function() { + if ( !$.ui.controlgroup ) { + $.error( "Controlgroup widget missing" ); + } + if ( arguments[ 0 ] === "option" && arguments[ 1 ] === "items" && arguments[ 2 ] ) { + return this.controlgroup.apply( this, + [ arguments[ 0 ], "items.button", arguments[ 2 ] ] ); + } + if ( arguments[ 0 ] === "option" && arguments[ 1 ] === "items" ) { + return this.controlgroup.apply( this, [ arguments[ 0 ], "items.button" ] ); + } + if ( typeof arguments[ 0 ] === "object" && arguments[ 0 ].items ) { + arguments[ 0 ].items = { + button: arguments[ 0 ].items + }; } + return this.controlgroup.apply( this, arguments ); + }; +} - options.active = collapsing ? false : this.headers.index( clicked ); +var widgetsButton = $.ui.button; - // When the call to ._toggle() comes after the class changes - // it causes a very odd bug in IE 8 (see #6720) - this.active = clickedIsActive ? $() : clicked; - this._toggle( eventData ); - // Switch classes - // corner classes on the previously active header stay after the animation - this._removeClass( active, "ui-accordion-header-active", "ui-state-active" ); - if ( options.icons ) { - activeChildren = active.children( ".ui-accordion-header-icon" ); - this._removeClass( activeChildren, null, options.icons.activeHeader ) - ._addClass( activeChildren, null, options.icons.header ); - } +/* eslint-disable max-len, camelcase */ +/*! + * jQuery UI Datepicker 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( !clickedIsActive ) { - this._removeClass( clicked, "ui-accordion-header-collapsed" ) - ._addClass( clicked, "ui-accordion-header-active", "ui-state-active" ); - if ( options.icons ) { - clickedChildren = clicked.children( ".ui-accordion-header-icon" ); - this._removeClass( clickedChildren, null, options.icons.header ) - ._addClass( clickedChildren, null, options.icons.activeHeader ); - } +//>>label: Datepicker +//>>group: Widgets +//>>description: Displays a calendar from an input or inline for selecting dates. +//>>docs: http://api.jqueryui.com/datepicker/ +//>>demos: http://jqueryui.com/datepicker/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/datepicker.css +//>>css.theme: ../../themes/base/theme.css - this._addClass( clicked.next(), "ui-accordion-content-active" ); - } - }, - _toggle: function( data ) { - var toShow = data.newPanel, - toHide = this.prevShow.length ? this.prevShow : data.oldPanel; +$.extend( $.ui, { datepicker: { version: "1.13.2" } } ); - // Handle activating a panel during the animation for another activation - this.prevShow.add( this.prevHide ).stop( true, true ); - this.prevShow = toShow; - this.prevHide = toHide; +var datepicker_instActive; - if ( this.options.animate ) { - this._animate( toShow, toHide, data ); - } else { - toHide.hide(); - toShow.show(); - this._toggleComplete( data ); - } +function datepicker_getZindex( elem ) { + var position, value; + while ( elem.length && elem[ 0 ] !== document ) { - toHide.attr( { - "aria-hidden": "true" - } ); - toHide.prev().attr( { - "aria-selected": "false", - "aria-expanded": "false" - } ); + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // if we're switching panels, remove the old header from the tab order - // if we're opening from collapsed state, remove the previous header from the tab order - // if we're collapsing, then keep the collapsing header in the tab order - if ( toShow.length && toHide.length ) { - toHide.prev().attr( { - "tabIndex": -1, - "aria-expanded": "false" - } ); - } else if ( toShow.length ) { - this.headers.filter( function() { - return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; - } ) - .attr( "tabIndex", -1 ); + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } } + elem = elem.parent(); + } - toShow - .attr( "aria-hidden", "false" ) - .prev() - .attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); - }, + return 0; +} - _animate: function( toShow, toHide, data ) { - var total, easing, duration, - that = this, - adjust = 0, - boxSizing = toShow.css( "box-sizing" ), - down = toShow.length && - ( !toHide.length || ( toShow.index() < toHide.index() ) ), - animate = this.options.animate || {}, - options = down && animate.down || animate, - complete = function() { - that._toggleComplete( data ); - }; +/* Date picker manager. + Use the singleton instance of this class, $.datepicker, to interact with the date picker. + Settings for (groups of) date pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ - if ( typeof options === "number" ) { - duration = options; - } - if ( typeof options === "string" ) { - easing = options; - } +function Datepicker() { + this._curInst = null; // The current instance in use + this._keyEvent = false; // If the last event was a key event + this._disabledInputs = []; // List of date picker inputs that have been disabled + this._datepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division + this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class + this._appendClass = "ui-datepicker-append"; // The name of the append marker class + this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class + this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class + this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class + this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class + this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class + this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class + this.regional = []; // Available regional settings, indexed by language code + this.regional[ "" ] = { // Default regional settings + closeText: "Done", // Display text for close link + prevText: "Prev", // Display text for previous month link + nextText: "Next", // Display text for next month link + currentText: "Today", // Display text for current month link + monthNames: [ "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" ], // Names of months for drop-down and formatting + monthNamesShort: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], // For formatting + dayNames: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], // For formatting + dayNamesShort: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], // For formatting + dayNamesMin: [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ], // Column headings for days starting at Sunday + weekHeader: "Wk", // Column header for week of the year + dateFormat: "mm/dd/yy", // See format options on parseDate + firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... + isRTL: false, // True if right-to-left language, false if left-to-right + showMonthAfterYear: false, // True if the year select precedes month, false for month then year + yearSuffix: "", // Additional text to append to the year in the month headers, + selectMonthLabel: "Select month", // Invisible label for month selector + selectYearLabel: "Select year" // Invisible label for year selector + }; + this._defaults = { // Global defaults for all the date picker instances + showOn: "focus", // "focus" for popup on focus, + // "button" for trigger button, or "both" for either + showAnim: "fadeIn", // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + defaultDate: null, // Used when field is blank: actual date, + // +/-number for offset from today, null for today + appendText: "", // Display text following the input box, e.g. showing the format + buttonText: "...", // Text for trigger button + buttonImage: "", // URL for trigger button image + buttonImageOnly: false, // True if the image appears alone, false if it appears on a button + hideIfNoPrevNext: false, // True to hide next/previous month links + // if not applicable, false to just disable them + navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links + gotoCurrent: false, // True if today link goes back to current selection instead + changeMonth: false, // True if month can be selected directly, false if only prev/next + changeYear: false, // True if year can be selected directly, false if only prev/next + yearRange: "c-10:c+10", // Range of years to display in drop-down, + // either relative to today's year (-nn:+nn), relative to currently displayed year + // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) + showOtherMonths: false, // True to show dates in other months, false to leave blank + selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable + showWeek: false, // True to show week of the year, false to not show it + calculateWeek: this.iso8601Week, // How to calculate the week of the year, + // takes a Date and returns the number of the week for it + shortYearCutoff: "+10", // Short year values < this are in the current century, + // > this are in the previous century, + // string value starting with "+" for current year + value + minDate: null, // The earliest selectable date, or null for no limit + maxDate: null, // The latest selectable date, or null for no limit + duration: "fast", // Duration of display/closure + beforeShowDay: null, // Function that takes a date and returns an array with + // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", + // [2] = cell title (optional), e.g. $.datepicker.noWeekends + beforeShow: null, // Function that takes an input field and + // returns a set of custom settings for the date picker + onSelect: null, // Define a callback function when a date is selected + onChangeMonthYear: null, // Define a callback function when the month or year is changed + onClose: null, // Define a callback function when the datepicker is closed + onUpdateDatepicker: null, // Define a callback function when the datepicker is updated + numberOfMonths: 1, // Number of months to show at a time + showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) + stepMonths: 1, // Number of months to step back/forward + stepBigMonths: 12, // Number of months to step back/forward for the big links + altField: "", // Selector for an alternate field to store selected dates into + altFormat: "", // The date format to use for the alternate field + constrainInput: true, // The input is constrained by the current date format + showButtonPanel: false, // True to show button panel, false to not show it + autoSize: false, // True to size the input for the date format, false to leave as is + disabled: false // The initial disabled state + }; + $.extend( this._defaults, this.regional[ "" ] ); + this.regional.en = $.extend( true, {}, this.regional[ "" ] ); + this.regional[ "en-US" ] = $.extend( true, {}, this.regional.en ); + this.dpDiv = datepicker_bindHover( $( "<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>" ) ); +} - // fall back from options to animation in case of partial down settings - easing = easing || options.easing || animate.easing; - duration = duration || options.duration || animate.duration; +$.extend( Datepicker.prototype, { - if ( !toHide.length ) { - return toShow.animate( this.showProps, duration, easing, complete ); - } - if ( !toShow.length ) { - return toHide.animate( this.hideProps, duration, easing, complete ); - } + /* Class name added to elements to indicate already configured with a date picker. */ + markerClassName: "hasDatepicker", - total = toShow.show().outerHeight(); - toHide.animate( this.hideProps, { - duration: duration, - easing: easing, - step: function( now, fx ) { - fx.now = Math.round( now ); - } - } ); - toShow - .hide() - .animate( this.showProps, { - duration: duration, - easing: easing, - complete: complete, - step: function( now, fx ) { - fx.now = Math.round( now ); - if ( fx.prop !== "height" ) { - if ( boxSizing === "content-box" ) { - adjust += fx.now; - } - } else if ( that.options.heightStyle !== "content" ) { - fx.now = Math.round( total - toHide.outerHeight() - adjust ); - adjust = 0; - } - } - } ); - }, + //Keep track of the maximum number of rows displayed (see #7043) + maxRows: 4, - _toggleComplete: function( data ) { - var toHide = data.oldPanel, - prev = toHide.prev(); + // TODO rename to "widget" when switching to widget factory + _widgetDatepicker: function() { + return this.dpDiv; + }, - this._removeClass( toHide, "ui-accordion-content-active" ); - this._removeClass( prev, "ui-accordion-header-active" ) - ._addClass( prev, "ui-accordion-header-collapsed" ); + /* Override the default settings for all instances of the date picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function( settings ) { + datepicker_extendRemove( this._defaults, settings || {} ); + return this; + }, - // Work around for rendering bug in IE (#5421) - if ( toHide.length ) { - toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; + /* Attach the date picker to a jQuery selection. + * @param target element - the target input field or division or span + * @param settings object - the new settings to use for this date picker instance (anonymous) + */ + _attachDatepicker: function( target, settings ) { + var nodeName, inline, inst; + nodeName = target.nodeName.toLowerCase(); + inline = ( nodeName === "div" || nodeName === "span" ); + if ( !target.id ) { + this.uuid += 1; + target.id = "dp" + this.uuid; + } + inst = this._newInst( $( target ), inline ); + inst.settings = $.extend( {}, settings || {} ); + if ( nodeName === "input" ) { + this._connectDatepicker( target, inst ); + } else if ( inline ) { + this._inlineDatepicker( target, inst ); } - this._trigger( "activate", null, data ); - } -} ); - - -/*! - * jQuery UI Menu 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Menu -//>>group: Widgets -//>>description: Creates nestable menus. -//>>docs: http://api.jqueryui.com/menu/ -//>>demos: http://jqueryui.com/menu/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/menu.css -//>>css.theme: ../../themes/base/theme.css - - -var widgetsMenu = $.widget( "ui.menu", { - version: "1.13.1", - defaultElement: "<ul>", - delay: 300, - options: { - icons: { - submenu: "ui-icon-caret-1-e" - }, - items: "> *", - menus: "ul", - position: { - my: "left top", - at: "right top" - }, - role: "menu", - - // Callbacks - blur: null, - focus: null, - select: null }, - _create: function() { - this.activeMenu = this.element; + /* Create a new instance object. */ + _newInst: function( target, inline ) { + var id = target[ 0 ].id.replace( /([^A-Za-z0-9_\-])/g, "\\\\$1" ); // escape jQuery meta chars + return { id: id, input: target, // associated target + selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection + drawMonth: 0, drawYear: 0, // month being drawn + inline: inline, // is datepicker inline or not + dpDiv: ( !inline ? this.dpDiv : // presentation div + datepicker_bindHover( $( "<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>" ) ) ) }; + }, - // Flag used to prevent firing of the click handler - // as the event bubbles up through nested menus - this.mouseHandled = false; - this.lastMousePosition = { x: null, y: null }; - this.element - .uniqueId() - .attr( { - role: this.options.role, - tabIndex: 0 - } ); + /* Attach the date picker to an input field. */ + _connectDatepicker: function( target, inst ) { + var input = $( target ); + inst.append = $( [] ); + inst.trigger = $( [] ); + if ( input.hasClass( this.markerClassName ) ) { + return; + } + this._attachments( input, inst ); + input.addClass( this.markerClassName ).on( "keydown", this._doKeyDown ). + on( "keypress", this._doKeyPress ).on( "keyup", this._doKeyUp ); + this._autoSize( inst ); + $.data( target, "datepicker", inst ); - this._addClass( "ui-menu", "ui-widget ui-widget-content" ); - this._on( { + //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) + if ( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + }, - // Prevent focus from sticking to links inside menu after clicking - // them (focus should always stay on UL during navigation). - "mousedown .ui-menu-item": function( event ) { - event.preventDefault(); + /* Make attachments based on settings. */ + _attachments: function( input, inst ) { + var showOn, buttonText, buttonImage, + appendText = this._get( inst, "appendText" ), + isRTL = this._get( inst, "isRTL" ); - this._activateItem( event ); - }, - "click .ui-menu-item": function( event ) { - var target = $( event.target ); - var active = $( $.ui.safeActiveElement( this.document[ 0 ] ) ); - if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) { - this.select( event ); + if ( inst.append ) { + inst.append.remove(); + } + if ( appendText ) { + inst.append = $( "<span>" ) + .addClass( this._appendClass ) + .text( appendText ); + input[ isRTL ? "before" : "after" ]( inst.append ); + } - // Only set the mouseHandled flag if the event will bubble, see #9469. - if ( !event.isPropagationStopped() ) { - this.mouseHandled = true; - } + input.off( "focus", this._showDatepicker ); - // Open submenu on click - if ( target.has( ".ui-menu" ).length ) { - this.expand( event ); - } else if ( !this.element.is( ":focus" ) && - active.closest( ".ui-menu" ).length ) { + if ( inst.trigger ) { + inst.trigger.remove(); + } - // Redirect focus to the menu - this.element.trigger( "focus", [ true ] ); + showOn = this._get( inst, "showOn" ); + if ( showOn === "focus" || showOn === "both" ) { // pop-up date picker when in the marked field + input.on( "focus", this._showDatepicker ); + } + if ( showOn === "button" || showOn === "both" ) { // pop-up date picker when button clicked + buttonText = this._get( inst, "buttonText" ); + buttonImage = this._get( inst, "buttonImage" ); - // If the active item is on the top level, let it stay active. - // Otherwise, blur the active item since it is no longer visible. - if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) { - clearTimeout( this.timer ); - } - } + if ( this._get( inst, "buttonImageOnly" ) ) { + inst.trigger = $( "<img>" ) + .addClass( this._triggerClass ) + .attr( { + src: buttonImage, + alt: buttonText, + title: buttonText + } ); + } else { + inst.trigger = $( "<button type='button'>" ) + .addClass( this._triggerClass ); + if ( buttonImage ) { + inst.trigger.html( + $( "<img>" ) + .attr( { + src: buttonImage, + alt: buttonText, + title: buttonText + } ) + ); + } else { + inst.trigger.text( buttonText ); } - }, - "mouseenter .ui-menu-item": "_activateItem", - "mousemove .ui-menu-item": "_activateItem", - mouseleave: "collapseAll", - "mouseleave .ui-menu": "collapseAll", - focus: function( event, keepActiveItem ) { - - // If there's already an active item, keep it active - // If not, activate the first item - var item = this.active || this._menuItems().first(); + } - if ( !keepActiveItem ) { - this.focus( event, item ); + input[ isRTL ? "before" : "after" ]( inst.trigger ); + inst.trigger.on( "click", function() { + if ( $.datepicker._datepickerShowing && $.datepicker._lastInput === input[ 0 ] ) { + $.datepicker._hideDatepicker(); + } else if ( $.datepicker._datepickerShowing && $.datepicker._lastInput !== input[ 0 ] ) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker( input[ 0 ] ); + } else { + $.datepicker._showDatepicker( input[ 0 ] ); } - }, - blur: function( event ) { - this._delay( function() { - var notContained = !$.contains( - this.element[ 0 ], - $.ui.safeActiveElement( this.document[ 0 ] ) - ); - if ( notContained ) { - this.collapseAll( event ); - } - } ); - }, - keydown: "_keydown" - } ); - - this.refresh(); + return false; + } ); + } + }, - // Clicks outside of a menu collapse any open menus - this._on( this.document, { - click: function( event ) { - if ( this._closeOnDocumentClick( event ) ) { - this.collapseAll( event, true ); - } + /* Apply the maximum length for the date format. */ + _autoSize: function( inst ) { + if ( this._get( inst, "autoSize" ) && !inst.inline ) { + var findMax, max, maxI, i, + date = new Date( 2009, 12 - 1, 20 ), // Ensure double digits + dateFormat = this._get( inst, "dateFormat" ); - // Reset the mouseHandled flag - this.mouseHandled = false; + if ( dateFormat.match( /[DM]/ ) ) { + findMax = function( names ) { + max = 0; + maxI = 0; + for ( i = 0; i < names.length; i++ ) { + if ( names[ i ].length > max ) { + max = names[ i ].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth( findMax( this._get( inst, ( dateFormat.match( /MM/ ) ? + "monthNames" : "monthNamesShort" ) ) ) ); + date.setDate( findMax( this._get( inst, ( dateFormat.match( /DD/ ) ? + "dayNames" : "dayNamesShort" ) ) ) + 20 - date.getDay() ); } - } ); + inst.input.attr( "size", this._formatDate( inst, date ).length ); + } }, - _activateItem: function( event ) { - - // Ignore mouse events while typeahead is active, see #10458. - // Prevents focusing the wrong item when typeahead causes a scroll while the mouse - // is over an item in the menu - if ( this.previousFilter ) { + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function( target, inst ) { + var divSpan = $( target ); + if ( divSpan.hasClass( this.markerClassName ) ) { return; } + divSpan.addClass( this.markerClassName ).append( inst.dpDiv ); + $.data( target, "datepicker", inst ); + this._setDate( inst, this._getDefaultDate( inst ), true ); + this._updateDatepicker( inst ); + this._updateAlternate( inst ); - // If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356) - if ( event.clientX === this.lastMousePosition.x && - event.clientY === this.lastMousePosition.y ) { - return; + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if ( inst.settings.disabled ) { + this._disableDatepicker( target ); } - this.lastMousePosition = { - x: event.clientX, - y: event.clientY - }; + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css( "display", "block" ); + }, - var actualTarget = $( event.target ).closest( ".ui-menu-item" ), - target = $( event.currentTarget ); + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function( input, date, onSelect, settings, pos ) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance - // Ignore bubbled events on parent items, see #11641 - if ( actualTarget[ 0 ] !== target[ 0 ] ) { - return; + if ( !inst ) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $( "<input type='text' id='" + id + + "' style='position: absolute; top: -100px; width: 0px;'/>" ); + this._dialogInput.on( "keydown", this._doKeyDown ); + $( "body" ).append( this._dialogInput ); + inst = this._dialogInst = this._newInst( this._dialogInput, false ); + inst.settings = {}; + $.data( this._dialogInput[ 0 ], "datepicker", inst ); } + datepicker_extendRemove( inst.settings, settings || {} ); + date = ( date && date.constructor === Date ? this._formatDate( inst, date ) : date ); + this._dialogInput.val( date ); - // If the item is already active, there's nothing to do - if ( target.is( ".ui-state-active" ) ) { - return; + this._pos = ( pos ? ( pos.length ? pos : [ pos.pageX, pos.pageY ] ) : null ); + if ( !this._pos ) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [ ( browserWidth / 2 ) - 100 + scrollX, ( browserHeight / 2 ) - 150 + scrollY ]; } - // Remove ui-state-active class from siblings of the newly focused menu item - // to avoid a jump caused by adjacent elements both having a class with a border - this._removeClass( target.siblings().children( ".ui-state-active" ), - null, "ui-state-active" ); - this.focus( event, target ); - }, - - _destroy: function() { - var items = this.element.find( ".ui-menu-item" ) - .removeAttr( "role aria-disabled" ), - submenus = items.children( ".ui-menu-item-wrapper" ) - .removeUniqueId() - .removeAttr( "tabIndex role aria-haspopup" ); - - // Destroy (sub)menus - this.element - .removeAttr( "aria-activedescendant" ) - .find( ".ui-menu" ).addBack() - .removeAttr( "role aria-labelledby aria-expanded aria-hidden aria-disabled " + - "tabIndex" ) - .removeUniqueId() - .show(); - - submenus.children().each( function() { - var elem = $( this ); - if ( elem.data( "ui-menu-submenu-caret" ) ) { - elem.remove(); - } - } ); + // Move input on screen for focus, but hidden behind dialog + this._dialogInput.css( "left", ( this._pos[ 0 ] + 20 ) + "px" ).css( "top", this._pos[ 1 ] + "px" ); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass( this._dialogClass ); + this._showDatepicker( this._dialogInput[ 0 ] ); + if ( $.blockUI ) { + $.blockUI( this.dpDiv ); + } + $.data( this._dialogInput[ 0 ], "datepicker", inst ); + return this; }, - _keydown: function( event ) { - var match, prev, character, skip, - preventDefault = true; - - switch ( event.keyCode ) { - case $.ui.keyCode.PAGE_UP: - this.previousPage( event ); - break; - case $.ui.keyCode.PAGE_DOWN: - this.nextPage( event ); - break; - case $.ui.keyCode.HOME: - this._move( "first", "first", event ); - break; - case $.ui.keyCode.END: - this._move( "last", "last", event ); - break; - case $.ui.keyCode.UP: - this.previous( event ); - break; - case $.ui.keyCode.DOWN: - this.next( event ); - break; - case $.ui.keyCode.LEFT: - this.collapse( event ); - break; - case $.ui.keyCode.RIGHT: - if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { - this.expand( event ); - } - break; - case $.ui.keyCode.ENTER: - case $.ui.keyCode.SPACE: - this._activate( event ); - break; - case $.ui.keyCode.ESCAPE: - this.collapse( event ); - break; - default: - preventDefault = false; - prev = this.previousFilter || ""; - skip = false; - - // Support number pad values - character = event.keyCode >= 96 && event.keyCode <= 105 ? - ( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode ); - - clearTimeout( this.filterTimer ); - - if ( character === prev ) { - skip = true; - } else { - character = prev + character; - } - - match = this._filterMenuItems( character ); - match = skip && match.index( this.active.next() ) !== -1 ? - this.active.nextAll( ".ui-menu-item" ) : - match; + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function( target ) { + var nodeName, + $target = $( target ), + inst = $.data( target, "datepicker" ); - // If no matches on the current filter, reset to the last character pressed - // to move down the menu to the first item that starts with that character - if ( !match.length ) { - character = String.fromCharCode( event.keyCode ); - match = this._filterMenuItems( character ); - } + if ( !$target.hasClass( this.markerClassName ) ) { + return; + } - if ( match.length ) { - this.focus( event, match ); - this.previousFilter = character; - this.filterTimer = this._delay( function() { - delete this.previousFilter; - }, 1000 ); - } else { - delete this.previousFilter; - } + nodeName = target.nodeName.toLowerCase(); + $.removeData( target, "datepicker" ); + if ( nodeName === "input" ) { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass( this.markerClassName ). + off( "focus", this._showDatepicker ). + off( "keydown", this._doKeyDown ). + off( "keypress", this._doKeyPress ). + off( "keyup", this._doKeyUp ); + } else if ( nodeName === "div" || nodeName === "span" ) { + $target.removeClass( this.markerClassName ).empty(); } - if ( preventDefault ) { - event.preventDefault(); + if ( datepicker_instActive === inst ) { + datepicker_instActive = null; + this._curInst = null; } }, - _activate: function( event ) { - if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { - if ( this.active.children( "[aria-haspopup='true']" ).length ) { - this.expand( event ); - } else { - this.select( event ); - } + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function( target ) { + var nodeName, inline, + $target = $( target ), + inst = $.data( target, "datepicker" ); + + if ( !$target.hasClass( this.markerClassName ) ) { + return; } - }, - refresh: function() { - var menus, items, newSubmenus, newItems, newWrappers, - that = this, - icon = this.options.icons.submenu, - submenus = this.element.find( this.options.menus ); + nodeName = target.nodeName.toLowerCase(); + if ( nodeName === "input" ) { + target.disabled = false; + inst.trigger.filter( "button" ). + each( function() { + this.disabled = false; + } ).end(). + filter( "img" ).css( { opacity: "1.0", cursor: "" } ); + } else if ( nodeName === "div" || nodeName === "span" ) { + inline = $target.children( "." + this._inlineClass ); + inline.children().removeClass( "ui-state-disabled" ); + inline.find( "select.ui-datepicker-month, select.ui-datepicker-year" ). + prop( "disabled", false ); + } + this._disabledInputs = $.map( this._disabledInputs, - this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length ); + // Delete entry + function( value ) { + return ( value === target ? null : value ); + } ); + }, - // Initialize nested menus - newSubmenus = submenus.filter( ":not(.ui-menu)" ) - .hide() - .attr( { - role: this.options.role, - "aria-hidden": "true", - "aria-expanded": "false" - } ) - .each( function() { - var menu = $( this ), - item = menu.prev(), - submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true ); - - that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon ); - item - .attr( "aria-haspopup", "true" ) - .prepend( submenuCaret ); - menu.attr( "aria-labelledby", item.attr( "id" ) ); - } ); - - this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" ); - - menus = submenus.add( this.element ); - items = menus.find( this.options.items ); - - // Initialize menu-items containing spaces and/or dashes only as dividers - items.not( ".ui-menu-item" ).each( function() { - var item = $( this ); - if ( that._isDivider( item ) ) { - that._addClass( item, "ui-menu-divider", "ui-widget-content" ); - } - } ); - - // Don't refresh list items that are already adapted - newItems = items.not( ".ui-menu-item, .ui-menu-divider" ); - newWrappers = newItems.children() - .not( ".ui-menu" ) - .uniqueId() - .attr( { - tabIndex: -1, - role: this._itemRole() - } ); - this._addClass( newItems, "ui-menu-item" ) - ._addClass( newWrappers, "ui-menu-item-wrapper" ); + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function( target ) { + var nodeName, inline, + $target = $( target ), + inst = $.data( target, "datepicker" ); - // Add aria-disabled attribute to any disabled menu item - items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); + if ( !$target.hasClass( this.markerClassName ) ) { + return; + } - // If the active item has been removed, blur the menu - if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { - this.blur(); + nodeName = target.nodeName.toLowerCase(); + if ( nodeName === "input" ) { + target.disabled = true; + inst.trigger.filter( "button" ). + each( function() { + this.disabled = true; + } ).end(). + filter( "img" ).css( { opacity: "0.5", cursor: "default" } ); + } else if ( nodeName === "div" || nodeName === "span" ) { + inline = $target.children( "." + this._inlineClass ); + inline.children().addClass( "ui-state-disabled" ); + inline.find( "select.ui-datepicker-month, select.ui-datepicker-year" ). + prop( "disabled", true ); } - }, + this._disabledInputs = $.map( this._disabledInputs, - _itemRole: function() { - return { - menu: "menuitem", - listbox: "option" - }[ this.options.role ]; + // Delete entry + function( value ) { + return ( value === target ? null : value ); + } ); + this._disabledInputs[ this._disabledInputs.length ] = target; }, - _setOption: function( key, value ) { - if ( key === "icons" ) { - var icons = this.element.find( ".ui-menu-icon" ); - this._removeClass( icons, null, this.options.icons.submenu ) - ._addClass( icons, null, value.submenu ); + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function( target ) { + if ( !target ) { + return false; } - this._super( key, value ); + for ( var i = 0; i < this._disabledInputs.length; i++ ) { + if ( this._disabledInputs[ i ] === target ) { + return true; + } + } + return false; }, - _setOptionDisabled: function( value ) { - this._super( value ); - - this.element.attr( "aria-disabled", String( value ) ); - this._toggleClass( null, "ui-state-disabled", !!value ); + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function( target ) { + try { + return $.data( target, "datepicker" ); + } catch ( err ) { + throw "Missing instance data for this datepicker"; + } }, - focus: function( event, item ) { - var nested, focused, activeParent; - this.blur( event, event && event.type === "focus" ); - - this._scrollIntoView( item ); - - this.active = item.first(); + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function( target, name, value ) { + var settings, date, minDate, maxDate, + inst = this._getInst( target ); - focused = this.active.children( ".ui-menu-item-wrapper" ); - this._addClass( focused, null, "ui-state-active" ); + if ( arguments.length === 2 && typeof name === "string" ) { + return ( name === "defaults" ? $.extend( {}, $.datepicker._defaults ) : + ( inst ? ( name === "all" ? $.extend( {}, inst.settings ) : + this._get( inst, name ) ) : null ) ); + } - // Only update aria-activedescendant if there's a role - // otherwise we assume focus is managed elsewhere - if ( this.options.role ) { - this.element.attr( "aria-activedescendant", focused.attr( "id" ) ); + settings = name || {}; + if ( typeof name === "string" ) { + settings = {}; + settings[ name ] = value; } - // Highlight active parent menu item, if any - activeParent = this.active - .parent() - .closest( ".ui-menu-item" ) - .children( ".ui-menu-item-wrapper" ); - this._addClass( activeParent, null, "ui-state-active" ); + if ( inst ) { + if ( this._curInst === inst ) { + this._hideDatepicker(); + } - if ( event && event.type === "keydown" ) { - this._close(); - } else { - this.timer = this._delay( function() { - this._close(); - }, this.delay ); - } + date = this._getDateDatepicker( target, true ); + minDate = this._getMinMaxDate( inst, "min" ); + maxDate = this._getMinMaxDate( inst, "max" ); + datepicker_extendRemove( inst.settings, settings ); - nested = item.children( ".ui-menu" ); - if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) { - this._startOpening( nested ); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if ( minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined ) { + inst.settings.minDate = this._formatDate( inst, minDate ); + } + if ( maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined ) { + inst.settings.maxDate = this._formatDate( inst, maxDate ); + } + if ( "disabled" in settings ) { + if ( settings.disabled ) { + this._disableDatepicker( target ); + } else { + this._enableDatepicker( target ); + } + } + this._attachments( $( target ), inst ); + this._autoSize( inst ); + this._setDate( inst, date ); + this._updateAlternate( inst ); + this._updateDatepicker( inst ); } - this.activeMenu = item.parent(); - - this._trigger( "focus", event, { item: item } ); }, - _scrollIntoView: function( item ) { - var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; - if ( this._hasScroll() ) { - borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0; - paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0; - offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; - scroll = this.activeMenu.scrollTop(); - elementHeight = this.activeMenu.height(); - itemHeight = item.outerHeight(); + // Change method deprecated + _changeDatepicker: function( target, name, value ) { + this._optionDatepicker( target, name, value ); + }, - if ( offset < 0 ) { - this.activeMenu.scrollTop( scroll + offset ); - } else if ( offset + itemHeight > elementHeight ) { - this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); - } + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function( target ) { + var inst = this._getInst( target ); + if ( inst ) { + this._updateDatepicker( inst ); } }, - blur: function( event, fromFocus ) { - if ( !fromFocus ) { - clearTimeout( this.timer ); + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function( target, date ) { + var inst = this._getInst( target ); + if ( inst ) { + this._setDate( inst, date ); + this._updateDatepicker( inst ); + this._updateAlternate( inst ); } + }, - if ( !this.active ) { - return; + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function( target, noDefault ) { + var inst = this._getInst( target ); + if ( inst && !inst.inline ) { + this._setDateFromField( inst, noDefault ); } - - this._removeClass( this.active.children( ".ui-menu-item-wrapper" ), - null, "ui-state-active" ); - - this._trigger( "blur", event, { item: this.active } ); - this.active = null; + return ( inst ? this._getDate( inst ) : null ); }, - _startOpening: function( submenu ) { - clearTimeout( this.timer ); + /* Handle keystrokes. */ + _doKeyDown: function( event ) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst( event.target ), + handled = true, + isRTL = inst.dpDiv.is( ".ui-datepicker-rtl" ); - // Don't open if already open fixes a Firefox bug that caused a .5 pixel - // shift in the submenu position when mousing over the caret icon - if ( submenu.attr( "aria-hidden" ) !== "true" ) { - return; - } + inst._keyEvent = true; + if ( $.datepicker._datepickerShowing ) { + switch ( event.keyCode ) { + case 9: $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: sel = $( "td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv ); + if ( sel[ 0 ] ) { + $.datepicker._selectDay( event.target, inst.selectedMonth, inst.selectedYear, sel[ 0 ] ); + } - this.timer = this._delay( function() { - this._close(); - this._open( submenu ); - }, this.delay ); - }, + onSelect = $.datepicker._get( inst, "onSelect" ); + if ( onSelect ) { + dateStr = $.datepicker._formatDate( inst ); - _open: function( submenu ) { - var position = $.extend( { - of: this.active - }, this.options.position ); + // Trigger custom callback + onSelect.apply( ( inst.input ? inst.input[ 0 ] : null ), [ dateStr, inst ] ); + } else { + $.datepicker._hideDatepicker(); + } - clearTimeout( this.timer ); - this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) ) - .hide() - .attr( "aria-hidden", "true" ); + return false; // don't submit the form + case 27: $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: $.datepicker._adjustDate( event.target, ( event.ctrlKey ? + -$.datepicker._get( inst, "stepBigMonths" ) : + -$.datepicker._get( inst, "stepMonths" ) ), "M" ); + break; // previous month/year on page up/+ ctrl + case 34: $.datepicker._adjustDate( event.target, ( event.ctrlKey ? + +$.datepicker._get( inst, "stepBigMonths" ) : + +$.datepicker._get( inst, "stepMonths" ) ), "M" ); + break; // next month/year on page down/+ ctrl + case 35: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._clearDate( event.target ); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._gotoToday( event.target ); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._adjustDate( event.target, ( isRTL ? +1 : -1 ), "D" ); + } + handled = event.ctrlKey || event.metaKey; - submenu - .show() - .removeAttr( "aria-hidden" ) - .attr( "aria-expanded", "true" ) - .position( position ); - }, + // -1 day on ctrl or command +left + if ( event.originalEvent.altKey ) { + $.datepicker._adjustDate( event.target, ( event.ctrlKey ? + -$.datepicker._get( inst, "stepBigMonths" ) : + -$.datepicker._get( inst, "stepMonths" ) ), "M" ); + } - collapseAll: function( event, all ) { - clearTimeout( this.timer ); - this.timer = this._delay( function() { + // next month/year on alt +left on Mac + break; + case 38: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._adjustDate( event.target, -7, "D" ); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._adjustDate( event.target, ( isRTL ? -1 : +1 ), "D" ); + } + handled = event.ctrlKey || event.metaKey; - // If we were passed an event, look for the submenu that contains the event - var currentMenu = all ? this.element : - $( event && event.target ).closest( this.element.find( ".ui-menu" ) ); + // +1 day on ctrl or command +right + if ( event.originalEvent.altKey ) { + $.datepicker._adjustDate( event.target, ( event.ctrlKey ? + +$.datepicker._get( inst, "stepBigMonths" ) : + +$.datepicker._get( inst, "stepMonths" ) ), "M" ); + } - // If we found no valid submenu ancestor, use the main menu to close all - // sub menus anyway - if ( !currentMenu.length ) { - currentMenu = this.element; + // next month/year on alt +right + break; + case 40: if ( event.ctrlKey || event.metaKey ) { + $.datepicker._adjustDate( event.target, +7, "D" ); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: handled = false; } - - this._close( currentMenu ); - - this.blur( event ); - - // Work around active item staying active after menu is blurred - this._removeClass( currentMenu.find( ".ui-state-active" ), null, "ui-state-active" ); - - this.activeMenu = currentMenu; - }, all ? 0 : this.delay ); - }, - - // With no arguments, closes the currently active menu - if nothing is active - // it closes all menus. If passed an argument, it will search for menus BELOW - _close: function( startMenu ) { - if ( !startMenu ) { - startMenu = this.active ? this.active.parent() : this.element; + } else if ( event.keyCode === 36 && event.ctrlKey ) { // display the date picker on ctrl+home + $.datepicker._showDatepicker( this ); + } else { + handled = false; } - startMenu.find( ".ui-menu" ) - .hide() - .attr( "aria-hidden", "true" ) - .attr( "aria-expanded", "false" ); - }, - - _closeOnDocumentClick: function( event ) { - return !$( event.target ).closest( ".ui-menu" ).length; + if ( handled ) { + event.preventDefault(); + event.stopPropagation(); + } }, - _isDivider: function( item ) { - - // Match hyphen, em dash, en dash - return !/[^\-\u2014\u2013\s]/.test( item.text() ); - }, + /* Filter entered characters - based on date format. */ + _doKeyPress: function( event ) { + var chars, chr, + inst = $.datepicker._getInst( event.target ); - collapse: function( event ) { - var newItem = this.active && - this.active.parent().closest( ".ui-menu-item", this.element ); - if ( newItem && newItem.length ) { - this._close(); - this.focus( event, newItem ); + if ( $.datepicker._get( inst, "constrainInput" ) ) { + chars = $.datepicker._possibleChars( $.datepicker._get( inst, "dateFormat" ) ); + chr = String.fromCharCode( event.charCode == null ? event.keyCode : event.charCode ); + return event.ctrlKey || event.metaKey || ( chr < " " || !chars || chars.indexOf( chr ) > -1 ); } }, - expand: function( event ) { - var newItem = this.active && this._menuItems( this.active.children( ".ui-menu" ) ).first(); + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function( event ) { + var date, + inst = $.datepicker._getInst( event.target ); - if ( newItem && newItem.length ) { - this._open( newItem.parent() ); + if ( inst.input.val() !== inst.lastVal ) { + try { + date = $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ), + ( inst.input ? inst.input.val() : null ), + $.datepicker._getFormatConfig( inst ) ); - // Delay so Firefox will not hide activedescendant change in expanding submenu from AT - this._delay( function() { - this.focus( event, newItem ); - } ); + if ( date ) { // only if valid + $.datepicker._setDateFromField( inst ); + $.datepicker._updateAlternate( inst ); + $.datepicker._updateDatepicker( inst ); + } + } catch ( err ) { + } } + return true; }, - next: function( event ) { - this._move( "next", "first", event ); - }, - - previous: function( event ) { - this._move( "prev", "last", event ); - }, - - isFirstItem: function() { - return this.active && !this.active.prevAll( ".ui-menu-item" ).length; - }, + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function( input ) { + input = input.target || input; + if ( input.nodeName.toLowerCase() !== "input" ) { // find from button/image trigger + input = $( "input", input.parentNode )[ 0 ]; + } - isLastItem: function() { - return this.active && !this.active.nextAll( ".ui-menu-item" ).length; - }, + if ( $.datepicker._isDisabledDatepicker( input ) || $.datepicker._lastInput === input ) { // already here + return; + } - _menuItems: function( menu ) { - return ( menu || this.element ) - .find( this.options.items ) - .filter( ".ui-menu-item" ); - }, + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; - _move: function( direction, filter, event ) { - var next; - if ( this.active ) { - if ( direction === "first" || direction === "last" ) { - next = this.active - [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) - .last(); - } else { - next = this.active - [ direction + "All" ]( ".ui-menu-item" ) - .first(); + inst = $.datepicker._getInst( input ); + if ( $.datepicker._curInst && $.datepicker._curInst !== inst ) { + $.datepicker._curInst.dpDiv.stop( true, true ); + if ( inst && $.datepicker._datepickerShowing ) { + $.datepicker._hideDatepicker( $.datepicker._curInst.input[ 0 ] ); } } - if ( !next || !next.length || !this.active ) { - next = this._menuItems( this.activeMenu )[ filter ](); + + beforeShow = $.datepicker._get( inst, "beforeShow" ); + beforeShowSettings = beforeShow ? beforeShow.apply( input, [ input, inst ] ) : {}; + if ( beforeShowSettings === false ) { + return; } + datepicker_extendRemove( inst.settings, beforeShowSettings ); - this.focus( event, next ); - }, + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField( inst ); - nextPage: function( event ) { - var item, base, height; - - if ( !this.active ) { - this.next( event ); - return; + if ( $.datepicker._inDialog ) { // hide cursor + input.value = ""; } - if ( this.isLastItem() ) { - return; + if ( !$.datepicker._pos ) { // position below input + $.datepicker._pos = $.datepicker._findPos( input ); + $.datepicker._pos[ 1 ] += input.offsetHeight; // add the height } - if ( this._hasScroll() ) { - base = this.active.offset().top; - height = this.element.innerHeight(); - // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. - if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { - height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); - } + isFixed = false; + $( input ).parents().each( function() { + isFixed |= $( this ).css( "position" ) === "fixed"; + return !isFixed; + } ); - this.active.nextAll( ".ui-menu-item" ).each( function() { - item = $( this ); - return item.offset().top - base - height < 0; - } ); + offset = { left: $.datepicker._pos[ 0 ], top: $.datepicker._pos[ 1 ] }; + $.datepicker._pos = null; - this.focus( event, item ); - } else { - this.focus( event, this._menuItems( this.activeMenu ) - [ !this.active ? "first" : "last" ]() ); - } - }, + //to avoid flashes on Firefox + inst.dpDiv.empty(); - previousPage: function( event ) { - var item, base, height; - if ( !this.active ) { - this.next( event ); - return; - } - if ( this.isFirstItem() ) { - return; - } - if ( this._hasScroll() ) { - base = this.active.offset().top; - height = this.element.innerHeight(); + // determine sizing offscreen + inst.dpDiv.css( { position: "absolute", display: "block", top: "-1000px" } ); + $.datepicker._updateDatepicker( inst ); - // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. - if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { - height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset( inst, offset, isFixed ); + inst.dpDiv.css( { position: ( $.datepicker._inDialog && $.blockUI ? + "static" : ( isFixed ? "fixed" : "absolute" ) ), display: "none", + left: offset.left + "px", top: offset.top + "px" } ); + + if ( !inst.inline ) { + showAnim = $.datepicker._get( inst, "showAnim" ); + duration = $.datepicker._get( inst, "duration" ); + inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 ); + $.datepicker._datepickerShowing = true; + + if ( $.effects && $.effects.effect[ showAnim ] ) { + inst.dpDiv.show( showAnim, $.datepicker._get( inst, "showOptions" ), duration ); + } else { + inst.dpDiv[ showAnim || "show" ]( showAnim ? duration : null ); } - this.active.prevAll( ".ui-menu-item" ).each( function() { - item = $( this ); - return item.offset().top - base + height > 0; - } ); + if ( $.datepicker._shouldFocusInput( inst ) ) { + inst.input.trigger( "focus" ); + } - this.focus( event, item ); - } else { - this.focus( event, this._menuItems( this.activeMenu ).first() ); + $.datepicker._curInst = inst; } }, - _hasScroll: function() { - return this.element.outerHeight() < this.element.prop( "scrollHeight" ); - }, + /* Generate the date picker content. */ + _updateDatepicker: function( inst ) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + datepicker_instActive = inst; // for delegate hover events + inst.dpDiv.empty().append( this._generateHTML( inst ) ); + this._attachHandlers( inst ); - select: function( event ) { + var origyearshtml, + numMonths = this._getNumberOfMonths( inst ), + cols = numMonths[ 1 ], + width = 17, + activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" ), + onUpdateDatepicker = $.datepicker._get( inst, "onUpdateDatepicker" ); - // TODO: It should never be possible to not have an active item at this - // point, but the tests don't trigger mouseenter before click. - this.active = this.active || $( event.target ).closest( ".ui-menu-item" ); - var ui = { item: this.active }; - if ( !this.active.has( ".ui-menu" ).length ) { - this.collapseAll( event, true ); + if ( activeCell.length > 0 ) { + datepicker_handleMouseover.apply( activeCell.get( 0 ) ); } - this._trigger( "select", event, ui ); - }, - _filterMenuItems: function( character ) { - var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ), - regex = new RegExp( "^" + escapedCharacter, "i" ); + inst.dpDiv.removeClass( "ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4" ).width( "" ); + if ( cols > 1 ) { + inst.dpDiv.addClass( "ui-datepicker-multi-" + cols ).css( "width", ( width * cols ) + "em" ); + } + inst.dpDiv[ ( numMonths[ 0 ] !== 1 || numMonths[ 1 ] !== 1 ? "add" : "remove" ) + + "Class" ]( "ui-datepicker-multi" ); + inst.dpDiv[ ( this._get( inst, "isRTL" ) ? "add" : "remove" ) + + "Class" ]( "ui-datepicker-rtl" ); - return this.activeMenu - .find( this.options.items ) + if ( inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { + inst.input.trigger( "focus" ); + } - // Only match on items, not dividers or other content (#10571) - .filter( ".ui-menu-item" ) - .filter( function() { - return regex.test( - String.prototype.trim.call( - $( this ).children( ".ui-menu-item-wrapper" ).text() ) ); - } ); - } -} ); + // Deffered render of the years select (to avoid flashes on Firefox) + if ( inst.yearshtml ) { + origyearshtml = inst.yearshtml; + setTimeout( function() { + //assure that inst.yearshtml didn't change. + if ( origyearshtml === inst.yearshtml && inst.yearshtml ) { + inst.dpDiv.find( "select.ui-datepicker-year" ).first().replaceWith( inst.yearshtml ); + } + origyearshtml = inst.yearshtml = null; + }, 0 ); + } -/*! - * jQuery UI Autocomplete 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + if ( onUpdateDatepicker ) { + onUpdateDatepicker.apply( ( inst.input ? inst.input[ 0 ] : null ), [ inst ] ); + } + }, -//>>label: Autocomplete -//>>group: Widgets -//>>description: Lists suggested words as the user is typing. -//>>docs: http://api.jqueryui.com/autocomplete/ -//>>demos: http://jqueryui.com/autocomplete/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/autocomplete.css -//>>css.theme: ../../themes/base/theme.css + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + // Support: IE and jQuery <1.9 + _shouldFocusInput: function( inst ) { + return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); + }, + /* Check positioning to remain on screen. */ + _checkOffset: function( inst, offset, isFixed ) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + ( isFixed ? 0 : $( document ).scrollLeft() ), + viewHeight = document.documentElement.clientHeight + ( isFixed ? 0 : $( document ).scrollTop() ); -$.widget( "ui.autocomplete", { - version: "1.13.1", - defaultElement: "<input>", - options: { - appendTo: null, - autoFocus: false, - delay: 300, - minLength: 1, - position: { - my: "left top", - at: "left bottom", - collision: "none" - }, - source: null, + offset.left -= ( this._get( inst, "isRTL" ) ? ( dpWidth - inputWidth ) : 0 ); + offset.left -= ( isFixed && offset.left === inst.input.offset().left ) ? $( document ).scrollLeft() : 0; + offset.top -= ( isFixed && offset.top === ( inst.input.offset().top + inputHeight ) ) ? $( document ).scrollTop() : 0; - // Callbacks - change: null, - close: null, - focus: null, - open: null, - response: null, - search: null, - select: null - }, + // Now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min( offset.left, ( offset.left + dpWidth > viewWidth && viewWidth > dpWidth ) ? + Math.abs( offset.left + dpWidth - viewWidth ) : 0 ); + offset.top -= Math.min( offset.top, ( offset.top + dpHeight > viewHeight && viewHeight > dpHeight ) ? + Math.abs( dpHeight + inputHeight ) : 0 ); - requestIndex: 0, - pending: 0, - liveRegionTimer: null, + return offset; + }, - _create: function() { + /* Find an object's position on the screen. */ + _findPos: function( obj ) { + var position, + inst = this._getInst( obj ), + isRTL = this._get( inst, "isRTL" ); - // Some browsers only repeat keydown events, not keypress events, - // so we use the suppressKeyPress flag to determine if we've already - // handled the keydown event. #7269 - // Unfortunately the code for & in keypress is the same as the up arrow, - // so we use the suppressKeyPressRepeat flag to avoid handling keypress - // events when we know the keydown event was used to modify the - // search term. #7799 - var suppressKeyPress, suppressKeyPressRepeat, suppressInput, - nodeName = this.element[ 0 ].nodeName.toLowerCase(), - isTextarea = nodeName === "textarea", - isInput = nodeName === "input"; + while ( obj && ( obj.type === "hidden" || obj.nodeType !== 1 || $.expr.pseudos.hidden( obj ) ) ) { + obj = obj[ isRTL ? "previousSibling" : "nextSibling" ]; + } - // Textareas are always multi-line - // Inputs are always single-line, even if inside a contentEditable element - // IE also treats inputs as contentEditable - // All other element types are determined by whether or not they're contentEditable - this.isMultiLine = isTextarea || !isInput && this._isContentEditable( this.element ); + position = $( obj ).offset(); + return [ position.left, position.top ]; + }, - this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; - this.isNewMenu = true; + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function( input ) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; - this._addClass( "ui-autocomplete-input" ); - this.element.attr( "autocomplete", "off" ); + if ( !inst || ( input && inst !== $.data( input, "datepicker" ) ) ) { + return; + } - this._on( this.element, { - keydown: function( event ) { - if ( this.element.prop( "readOnly" ) ) { - suppressKeyPress = true; - suppressInput = true; - suppressKeyPressRepeat = true; - return; - } - - suppressKeyPress = false; - suppressInput = false; - suppressKeyPressRepeat = false; - var keyCode = $.ui.keyCode; - switch ( event.keyCode ) { - case keyCode.PAGE_UP: - suppressKeyPress = true; - this._move( "previousPage", event ); - break; - case keyCode.PAGE_DOWN: - suppressKeyPress = true; - this._move( "nextPage", event ); - break; - case keyCode.UP: - suppressKeyPress = true; - this._keyEvent( "previous", event ); - break; - case keyCode.DOWN: - suppressKeyPress = true; - this._keyEvent( "next", event ); - break; - case keyCode.ENTER: - - // when menu is open and has focus - if ( this.menu.active ) { + if ( this._datepickerShowing ) { + showAnim = this._get( inst, "showAnim" ); + duration = this._get( inst, "duration" ); + postProcess = function() { + $.datepicker._tidyDialog( inst ); + }; - // #6055 - Opera still allows the keypress to occur - // which causes forms to submit - suppressKeyPress = true; - event.preventDefault(); - this.menu.select( event ); - } - break; - case keyCode.TAB: - if ( this.menu.active ) { - this.menu.select( event ); - } - break; - case keyCode.ESCAPE: - if ( this.menu.element.is( ":visible" ) ) { - if ( !this.isMultiLine ) { - this._value( this.term ); - } - this.close( event ); + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { + inst.dpDiv.hide( showAnim, $.datepicker._get( inst, "showOptions" ), duration, postProcess ); + } else { + inst.dpDiv[ ( showAnim === "slideDown" ? "slideUp" : + ( showAnim === "fadeIn" ? "fadeOut" : "hide" ) ) ]( ( showAnim ? duration : null ), postProcess ); + } - // Different browsers have different default behavior for escape - // Single press can mean undo or clear - // Double press in IE means clear the whole form - event.preventDefault(); - } - break; - default: - suppressKeyPressRepeat = true; + if ( !showAnim ) { + postProcess(); + } + this._datepickerShowing = false; - // search timeout should be triggered before the input value is changed - this._searchTimeout( event ); - break; - } - }, - keypress: function( event ) { - if ( suppressKeyPress ) { - suppressKeyPress = false; - if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { - event.preventDefault(); - } - return; - } - if ( suppressKeyPressRepeat ) { - return; - } + onClose = this._get( inst, "onClose" ); + if ( onClose ) { + onClose.apply( ( inst.input ? inst.input[ 0 ] : null ), [ ( inst.input ? inst.input.val() : "" ), inst ] ); + } - // Replicate some key handlers to allow them to repeat in Firefox and Opera - var keyCode = $.ui.keyCode; - switch ( event.keyCode ) { - case keyCode.PAGE_UP: - this._move( "previousPage", event ); - break; - case keyCode.PAGE_DOWN: - this._move( "nextPage", event ); - break; - case keyCode.UP: - this._keyEvent( "previous", event ); - break; - case keyCode.DOWN: - this._keyEvent( "next", event ); - break; - } - }, - input: function( event ) { - if ( suppressInput ) { - suppressInput = false; - event.preventDefault(); - return; + this._lastInput = null; + if ( this._inDialog ) { + this._dialogInput.css( { position: "absolute", left: "0", top: "-100px" } ); + if ( $.blockUI ) { + $.unblockUI(); + $( "body" ).append( this.dpDiv ); } - this._searchTimeout( event ); - }, - focus: function() { - this.selectedItem = null; - this.previous = this._value(); - }, - blur: function( event ) { - clearTimeout( this.searching ); - this.close( event ); - this._change( event ); } - } ); + this._inDialog = false; + } + }, - this._initSource(); - this.menu = $( "<ul>" ) - .appendTo( this._appendTo() ) - .menu( { + /* Tidy up after a dialog display. */ + _tidyDialog: function( inst ) { + inst.dpDiv.removeClass( this._dialogClass ).off( ".ui-datepicker-calendar" ); + }, - // disable ARIA support, the live region takes care of that - role: null - } ) - .hide() + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function( event ) { + if ( !$.datepicker._curInst ) { + return; + } - // Support: IE 11 only, Edge <= 14 - // For other browsers, we preventDefault() on the mousedown event - // to keep the dropdown from taking focus from the input. This doesn't - // work for IE/Edge, causing problems with selection and scrolling (#9638) - // Happily, IE and Edge support an "unselectable" attribute that - // prevents an element from receiving focus, exactly what we want here. - .attr( { - "unselectable": "on" - } ) - .menu( "instance" ); + var $target = $( event.target ), + inst = $.datepicker._getInst( $target[ 0 ] ); - this._addClass( this.menu.element, "ui-autocomplete", "ui-front" ); - this._on( this.menu.element, { - mousedown: function( event ) { + if ( ( ( $target[ 0 ].id !== $.datepicker._mainDivId && + $target.parents( "#" + $.datepicker._mainDivId ).length === 0 && + !$target.hasClass( $.datepicker.markerClassName ) && + !$target.closest( "." + $.datepicker._triggerClass ).length && + $.datepicker._datepickerShowing && !( $.datepicker._inDialog && $.blockUI ) ) ) || + ( $target.hasClass( $.datepicker.markerClassName ) && $.datepicker._curInst !== inst ) ) { + $.datepicker._hideDatepicker(); + } + }, - // Prevent moving focus out of the text field - event.preventDefault(); - }, - menufocus: function( event, ui ) { - var label, item; + /* Adjust one of the date sub-fields. */ + _adjustDate: function( id, offset, period ) { + var target = $( id ), + inst = this._getInst( target[ 0 ] ); - // support: Firefox - // Prevent accidental activation of menu items in Firefox (#7024 #9118) - if ( this.isNewMenu ) { - this.isNewMenu = false; - if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) { - this.menu.blur(); + if ( this._isDisabledDatepicker( target[ 0 ] ) ) { + return; + } + this._adjustInstDate( inst, offset, period ); + this._updateDatepicker( inst ); + }, - this.document.one( "mousemove", function() { - $( event.target ).trigger( event.originalEvent ); - } ); + /* Action for current link. */ + _gotoToday: function( id ) { + var date, + target = $( id ), + inst = this._getInst( target[ 0 ] ); - return; - } - } + if ( this._get( inst, "gotoCurrent" ) && inst.currentDay ) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange( inst ); + this._adjustDate( target ); + }, - item = ui.item.data( "ui-autocomplete-item" ); - if ( false !== this._trigger( "focus", event, { item: item } ) ) { + /* Action for selecting a new month/year. */ + _selectMonthYear: function( id, select, period ) { + var target = $( id ), + inst = this._getInst( target[ 0 ] ); - // use value to match what will end up in the input, if it was a key event - if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) { - this._value( item.value ); - } - } + inst[ "selected" + ( period === "M" ? "Month" : "Year" ) ] = + inst[ "draw" + ( period === "M" ? "Month" : "Year" ) ] = + parseInt( select.options[ select.selectedIndex ].value, 10 ); - // Announce the value in the liveRegion - label = ui.item.attr( "aria-label" ) || item.value; - if ( label && String.prototype.trim.call( label ).length ) { - clearTimeout( this.liveRegionTimer ); - this.liveRegionTimer = this._delay( function() { - this.liveRegion.html( $( "<div>" ).text( label ) ); - }, 100 ); - } - }, - menuselect: function( event, ui ) { - var item = ui.item.data( "ui-autocomplete-item" ), - previous = this.previous; + this._notifyChange( inst ); + this._adjustDate( target ); + }, - // Only trigger when focus was lost (click on menu) - if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) { - this.element.trigger( "focus" ); - this.previous = previous; - - // #6109 - IE triggers two focus events and the second - // is asynchronous, so we need to reset the previous - // term synchronously and asynchronously :-( - this._delay( function() { - this.previous = previous; - this.selectedItem = item; - } ); - } - - if ( false !== this._trigger( "select", event, { item: item } ) ) { - this._value( item.value ); - } - - // reset the term after the select event - // this allows custom select handling to work properly - this.term = this._value(); - - this.close( event ); - this.selectedItem = item; - } - } ); - - this.liveRegion = $( "<div>", { - role: "status", - "aria-live": "assertive", - "aria-relevant": "additions" - } ) - .appendTo( this.document[ 0 ].body ); - - this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" ); - - // Turning off autocomplete prevents the browser from remembering the - // value when navigating through history, so we re-enable autocomplete - // if the page is unloaded before the widget is destroyed. #7790 - this._on( this.window, { - beforeunload: function() { - this.element.removeAttr( "autocomplete" ); - } - } ); - }, - - _destroy: function() { - clearTimeout( this.searching ); - this.element.removeAttr( "autocomplete" ); - this.menu.element.remove(); - this.liveRegion.remove(); - }, + /* Action for selecting a day. */ + _selectDay: function( id, month, year, td ) { + var inst, + target = $( id ); - _setOption: function( key, value ) { - this._super( key, value ); - if ( key === "source" ) { - this._initSource(); - } - if ( key === "appendTo" ) { - this.menu.element.appendTo( this._appendTo() ); - } - if ( key === "disabled" && value && this.xhr ) { - this.xhr.abort(); + if ( $( td ).hasClass( this._unselectableClass ) || this._isDisabledDatepicker( target[ 0 ] ) ) { + return; } - }, - - _isEventTargetInWidget: function( event ) { - var menuElement = this.menu.element[ 0 ]; - return event.target === this.element[ 0 ] || - event.target === menuElement || - $.contains( menuElement, event.target ); + inst = this._getInst( target[ 0 ] ); + inst.selectedDay = inst.currentDay = parseInt( $( "a", td ).attr( "data-date" ) ); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate( id, this._formatDate( inst, + inst.currentDay, inst.currentMonth, inst.currentYear ) ); }, - _closeOnClickOutside: function( event ) { - if ( !this._isEventTargetInWidget( event ) ) { - this.close(); - } + /* Erase the input field and hide the date picker. */ + _clearDate: function( id ) { + var target = $( id ); + this._selectDate( target, "" ); }, - _appendTo: function() { - var element = this.options.appendTo; - - if ( element ) { - element = element.jquery || element.nodeType ? - $( element ) : - this.document.find( element ).eq( 0 ); - } + /* Update the input field with the selected date. */ + _selectDate: function( id, dateStr ) { + var onSelect, + target = $( id ), + inst = this._getInst( target[ 0 ] ); - if ( !element || !element[ 0 ] ) { - element = this.element.closest( ".ui-front, dialog" ); + dateStr = ( dateStr != null ? dateStr : this._formatDate( inst ) ); + if ( inst.input ) { + inst.input.val( dateStr ); } + this._updateAlternate( inst ); - if ( !element.length ) { - element = this.document[ 0 ].body; + onSelect = this._get( inst, "onSelect" ); + if ( onSelect ) { + onSelect.apply( ( inst.input ? inst.input[ 0 ] : null ), [ dateStr, inst ] ); // trigger custom callback + } else if ( inst.input ) { + inst.input.trigger( "change" ); // fire the change event } - return element; - }, - - _initSource: function() { - var array, url, - that = this; - if ( Array.isArray( this.options.source ) ) { - array = this.options.source; - this.source = function( request, response ) { - response( $.ui.autocomplete.filter( array, request.term ) ); - }; - } else if ( typeof this.options.source === "string" ) { - url = this.options.source; - this.source = function( request, response ) { - if ( that.xhr ) { - that.xhr.abort(); - } - that.xhr = $.ajax( { - url: url, - data: request, - dataType: "json", - success: function( data ) { - response( data ); - }, - error: function() { - response( [] ); - } - } ); - }; + if ( inst.inline ) { + this._updateDatepicker( inst ); } else { - this.source = this.options.source; - } - }, - - _searchTimeout: function( event ) { - clearTimeout( this.searching ); - this.searching = this._delay( function() { - - // Search if the value has changed, or if the user retypes the same value (see #7434) - var equalValues = this.term === this._value(), - menuVisible = this.menu.element.is( ":visible" ), - modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; - - if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) { - this.selectedItem = null; - this.search( null, event ); + this._hideDatepicker(); + this._lastInput = inst.input[ 0 ]; + if ( typeof( inst.input[ 0 ] ) !== "object" ) { + inst.input.trigger( "focus" ); // restore focus } - }, this.options.delay ); + this._lastInput = null; + } }, - search: function( value, event ) { - value = value != null ? value : this._value(); - - // Always save the actual value, not the one passed as an argument - this.term = this._value(); - - if ( value.length < this.options.minLength ) { - return this.close( event ); - } + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function( inst ) { + var altFormat, date, dateStr, + altField = this._get( inst, "altField" ); - if ( this._trigger( "search", event ) === false ) { - return; + if ( altField ) { // update alternate field too + altFormat = this._get( inst, "altFormat" ) || this._get( inst, "dateFormat" ); + date = this._getDate( inst ); + dateStr = this.formatDate( altFormat, date, this._getFormatConfig( inst ) ); + $( document ).find( altField ).val( dateStr ); } - - return this._search( value ); }, - _search: function( value ) { - this.pending++; - this._addClass( "ui-autocomplete-loading" ); - this.cancelSearch = false; - - this.source( { term: value }, this._response() ); + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function( date ) { + var day = date.getDay(); + return [ ( day > 0 && day < 6 ), "" ]; }, - _response: function() { - var index = ++this.requestIndex; + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function( date ) { + var time, + checkDate = new Date( date.getTime() ); - return function( content ) { - if ( index === this.requestIndex ) { - this.__response( content ); - } + // Find Thursday of this week starting on Monday + checkDate.setDate( checkDate.getDate() + 4 - ( checkDate.getDay() || 7 ) ); - this.pending--; - if ( !this.pending ) { - this._removeClass( "ui-autocomplete-loading" ); - } - }.bind( this ); + time = checkDate.getTime(); + checkDate.setMonth( 0 ); // Compare with Jan 1 + checkDate.setDate( 1 ); + return Math.floor( Math.round( ( time - checkDate ) / 86400000 ) / 7 ) + 1; }, - __response: function( content ) { - if ( content ) { - content = this._normalize( content ); + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function( format, value, settings ) { + if ( format == null || value == null ) { + throw "Invalid arguments"; } - this._trigger( "response", null, { content: content } ); - if ( !this.options.disabled && content && content.length && !this.cancelSearch ) { - this._suggest( content ); - this._trigger( "open" ); - } else { - // use ._close() instead of .close() so we don't cancel future searches - this._close(); + value = ( typeof value === "object" ? value.toString() : value + "" ); + if ( value === "" ) { + return null; } - }, - close: function( event ) { - this.cancelSearch = true; - this._close( event ); - }, + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = ( settings ? settings.shortYearCutoff : null ) || this._defaults.shortYearCutoff, + shortYearCutoff = ( typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt( shortYearCutoffTemp, 10 ) ), + dayNamesShort = ( settings ? settings.dayNamesShort : null ) || this._defaults.dayNamesShort, + dayNames = ( settings ? settings.dayNames : null ) || this._defaults.dayNames, + monthNamesShort = ( settings ? settings.monthNamesShort : null ) || this._defaults.monthNamesShort, + monthNames = ( settings ? settings.monthNames : null ) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, - _close: function( event ) { + // Check whether a format character is doubled + lookAhead = function( match ) { + var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); + if ( matches ) { + iFormat++; + } + return matches; + }, - // Remove the handler that closes the menu on outside clicks - this._off( this.document, "mousedown" ); + // Extract a number from the string value + getNumber = function( match ) { + var isDoubled = lookAhead( match ), + size = ( match === "@" ? 14 : ( match === "!" ? 20 : + ( match === "y" && isDoubled ? 4 : ( match === "o" ? 3 : 2 ) ) ) ), + minSize = ( match === "y" ? size : 1 ), + digits = new RegExp( "^\\d{" + minSize + "," + size + "}" ), + num = value.substring( iValue ).match( digits ); + if ( !num ) { + throw "Missing number at position " + iValue; + } + iValue += num[ 0 ].length; + return parseInt( num[ 0 ], 10 ); + }, - if ( this.menu.element.is( ":visible" ) ) { - this.menu.element.hide(); - this.menu.blur(); - this.isNewMenu = true; - this._trigger( "close", event ); - } - }, + // Extract a name from the string value and convert to an index + getName = function( match, shortNames, longNames ) { + var index = -1, + names = $.map( lookAhead( match ) ? longNames : shortNames, function( v, k ) { + return [ [ k, v ] ]; + } ).sort( function( a, b ) { + return -( a[ 1 ].length - b[ 1 ].length ); + } ); - _change: function( event ) { - if ( this.previous !== this._value() ) { - this._trigger( "change", event, { item: this.selectedItem } ); - } - }, + $.each( names, function( i, pair ) { + var name = pair[ 1 ]; + if ( value.substr( iValue, name.length ).toLowerCase() === name.toLowerCase() ) { + index = pair[ 0 ]; + iValue += name.length; + return false; + } + } ); + if ( index !== -1 ) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, - _normalize: function( items ) { + // Confirm that a literal character matches the string value + checkLiteral = function() { + if ( value.charAt( iValue ) !== format.charAt( iFormat ) ) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; - // assume all items have the right format when the first item is complete - if ( items.length && items[ 0 ].label && items[ 0 ].value ) { - return items; - } - return $.map( items, function( item ) { - if ( typeof item === "string" ) { - return { - label: item, - value: item - }; + for ( iFormat = 0; iFormat < format.length; iFormat++ ) { + if ( literal ) { + if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch ( format.charAt( iFormat ) ) { + case "d": + day = getNumber( "d" ); + break; + case "D": + getName( "D", dayNamesShort, dayNames ); + break; + case "o": + doy = getNumber( "o" ); + break; + case "m": + month = getNumber( "m" ); + break; + case "M": + month = getName( "M", monthNamesShort, monthNames ); + break; + case "y": + year = getNumber( "y" ); + break; + case "@": + date = new Date( getNumber( "@" ) ); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date( ( getNumber( "!" ) - this._ticksTo1970 ) / 10000 ); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if ( lookAhead( "'" ) ) { + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } } - return $.extend( {}, item, { - label: item.label || item.value, - value: item.value || item.label - } ); - } ); - }, - - _suggest: function( items ) { - var ul = this.menu.element.empty(); - this._renderMenu( ul, items ); - this.isNewMenu = true; - this.menu.refresh(); - - // Size and position menu - ul.show(); - this._resizeMenu(); - ul.position( $.extend( { - of: this.element - }, this.options.position ) ); - - if ( this.options.autoFocus ) { - this.menu.next(); - } - - // Listen for interactions outside of the widget (#6642) - this._on( this.document, { - mousedown: "_closeOnClickOutside" - } ); - }, - - _resizeMenu: function() { - var ul = this.menu.element; - ul.outerWidth( Math.max( - - // Firefox wraps long text (possibly a rounding bug) - // so we add 1px to avoid the wrapping (#7513) - ul.width( "" ).outerWidth() + 1, - this.element.outerWidth() - ) ); - }, - - _renderMenu: function( ul, items ) { - var that = this; - $.each( items, function( index, item ) { - that._renderItemData( ul, item ); - } ); - }, - - _renderItemData: function( ul, item ) { - return this._renderItem( ul, item ).data( "ui-autocomplete-item", item ); - }, - - _renderItem: function( ul, item ) { - return $( "<li>" ) - .append( $( "<div>" ).text( item.label ) ) - .appendTo( ul ); - }, - - _move: function( direction, event ) { - if ( !this.menu.element.is( ":visible" ) ) { - this.search( null, event ); - return; } - if ( this.menu.isFirstItem() && /^previous/.test( direction ) || - this.menu.isLastItem() && /^next/.test( direction ) ) { - if ( !this.isMultiLine ) { - this._value( this.term ); + if ( iValue < value.length ) { + extra = value.substr( iValue ); + if ( !/^\s+/.test( extra ) ) { + throw "Extra/unparsed characters found in date: " + extra; } - - this.menu.blur(); - return; } - this.menu[ direction ]( event ); - }, - - widget: function() { - return this.menu.element; - }, - _value: function() { - return this.valueMethod.apply( this.element, arguments ); - }, + if ( year === -1 ) { + year = new Date().getFullYear(); + } else if ( year < 100 ) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + ( year <= shortYearCutoff ? 0 : -100 ); + } - _keyEvent: function( keyEvent, event ) { - if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { - this._move( keyEvent, event ); + if ( doy > -1 ) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth( year, month - 1 ); + if ( day <= dim ) { + break; + } + month++; + day -= dim; + } while ( true ); + } - // Prevents moving cursor to beginning/end of the text field in some browsers - event.preventDefault(); + date = this._daylightSavingAdjust( new Date( year, month - 1, day ) ); + if ( date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day ) { + throw "Invalid date"; // E.g. 31/02/00 } + return date; }, - // Support: Chrome <=50 - // We should be able to just use this.element.prop( "isContentEditable" ) - // but hidden elements always report false in Chrome. - // https://code.google.com/p/chromium/issues/detail?id=313082 - _isContentEditable: function( element ) { - if ( !element.length ) { - return false; - } + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 - var editable = element.prop( "contentEditable" ); + _ticksTo1970: ( ( ( 1970 - 1 ) * 365 + Math.floor( 1970 / 4 ) - Math.floor( 1970 / 100 ) + + Math.floor( 1970 / 400 ) ) * 24 * 60 * 60 * 10000000 ), - if ( editable === "inherit" ) { - return this._isContentEditable( element.parent() ); + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function( format, date, settings ) { + if ( !date ) { + return ""; } - return editable === "true"; - } -} ); + var iFormat, + dayNamesShort = ( settings ? settings.dayNamesShort : null ) || this._defaults.dayNamesShort, + dayNames = ( settings ? settings.dayNames : null ) || this._defaults.dayNames, + monthNamesShort = ( settings ? settings.monthNamesShort : null ) || this._defaults.monthNamesShort, + monthNames = ( settings ? settings.monthNames : null ) || this._defaults.monthNames, -$.extend( $.ui.autocomplete, { - escapeRegex: function( value ) { - return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); - }, - filter: function( array, term ) { - var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" ); - return $.grep( array, function( value ) { - return matcher.test( value.label || value.value || value ); - } ); - } -} ); + // Check whether a format character is doubled + lookAhead = function( match ) { + var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); + if ( matches ) { + iFormat++; + } + return matches; + }, -// Live region extension, adding a `messages` option -// NOTE: This is an experimental API. We are still investigating -// a full solution for string manipulation and internationalization. -$.widget( "ui.autocomplete", $.ui.autocomplete, { - options: { - messages: { - noResults: "No search results.", - results: function( amount ) { - return amount + ( amount > 1 ? " results are" : " result is" ) + - " available, use up and down arrow keys to navigate."; + // Format a number, with leading zero if necessary + formatNumber = function( match, value, len ) { + var num = "" + value; + if ( lookAhead( match ) ) { + while ( num.length < len ) { + num = "0" + num; + } + } + return num; + }, + + // Format a name, short or long as requested + formatName = function( match, value, shortNames, longNames ) { + return ( lookAhead( match ) ? longNames[ value ] : shortNames[ value ] ); + }, + output = "", + literal = false; + + if ( date ) { + for ( iFormat = 0; iFormat < format.length; iFormat++ ) { + if ( literal ) { + if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { + literal = false; + } else { + output += format.charAt( iFormat ); + } + } else { + switch ( format.charAt( iFormat ) ) { + case "d": + output += formatNumber( "d", date.getDate(), 2 ); + break; + case "D": + output += formatName( "D", date.getDay(), dayNamesShort, dayNames ); + break; + case "o": + output += formatNumber( "o", + Math.round( ( new Date( date.getFullYear(), date.getMonth(), date.getDate() ).getTime() - new Date( date.getFullYear(), 0, 0 ).getTime() ) / 86400000 ), 3 ); + break; + case "m": + output += formatNumber( "m", date.getMonth() + 1, 2 ); + break; + case "M": + output += formatName( "M", date.getMonth(), monthNamesShort, monthNames ); + break; + case "y": + output += ( lookAhead( "y" ) ? date.getFullYear() : + ( date.getFullYear() % 100 < 10 ? "0" : "" ) + date.getFullYear() % 100 ); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if ( lookAhead( "'" ) ) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt( iFormat ); + } + } } } + return output; }, - __response: function( content ) { - var message; - this._superApply( arguments ); - if ( this.options.disabled || this.cancelSearch ) { - return; - } - if ( content && content.length ) { - message = this.options.messages.results( content.length ); - } else { - message = this.options.messages.noResults; - } - clearTimeout( this.liveRegionTimer ); - this.liveRegionTimer = this._delay( function() { - this.liveRegion.html( $( "<div>" ).text( message ) ); - }, 100 ); - } -} ); - -var widgetsAutocomplete = $.ui.autocomplete; - - -/*! - * jQuery UI Controlgroup 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Controlgroup -//>>group: Widgets -//>>description: Visually groups form control widgets -//>>docs: http://api.jqueryui.com/controlgroup/ -//>>demos: http://jqueryui.com/controlgroup/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/controlgroup.css -//>>css.theme: ../../themes/base/theme.css - + /* Extract all possible characters from the date format. */ + _possibleChars: function( format ) { + var iFormat, + chars = "", + literal = false, -var controlgroupCornerRegex = /ui-corner-([a-z]){2,6}/g; + // Check whether a format character is doubled + lookAhead = function( match ) { + var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); + if ( matches ) { + iFormat++; + } + return matches; + }; -var widgetsControlgroup = $.widget( "ui.controlgroup", { - version: "1.13.1", - defaultElement: "<div>", - options: { - direction: "horizontal", - disabled: null, - onlyVisible: true, - items: { - "button": "input[type=button], input[type=submit], input[type=reset], button, a", - "controlgroupLabel": ".ui-controlgroup-label", - "checkboxradio": "input[type='checkbox'], input[type='radio']", - "selectmenu": "select", - "spinner": ".ui-spinner-input" + for ( iFormat = 0; iFormat < format.length; iFormat++ ) { + if ( literal ) { + if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { + literal = false; + } else { + chars += format.charAt( iFormat ); + } + } else { + switch ( format.charAt( iFormat ) ) { + case "d": case "m": case "y": case "@": + chars += "0123456789"; + break; + case "D": case "M": + return null; // Accept anything + case "'": + if ( lookAhead( "'" ) ) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt( iFormat ); + } + } } + return chars; }, - _create: function() { - this._enhance(); + /* Get a setting value, defaulting if necessary. */ + _get: function( inst, name ) { + return inst.settings[ name ] !== undefined ? + inst.settings[ name ] : this._defaults[ name ]; }, - // To support the enhanced option in jQuery Mobile, we isolate DOM manipulation - _enhance: function() { - this.element.attr( "role", "toolbar" ); - this.refresh(); - }, - - _destroy: function() { - this._callChildMethod( "destroy" ); - this.childWidgets.removeData( "ui-controlgroup-data" ); - this.element.removeAttr( "role" ); - if ( this.options.items.controlgroupLabel ) { - this.element - .find( this.options.items.controlgroupLabel ) - .find( ".ui-controlgroup-label-contents" ) - .contents().unwrap(); + /* Parse existing date and initialise date picker. */ + _setDateFromField: function( inst, noDefault ) { + if ( inst.input.val() === inst.lastVal ) { + return; } - }, - - _initWidgets: function() { - var that = this, - childWidgets = []; - - // First we iterate over each of the items options - $.each( this.options.items, function( widget, selector ) { - var labels; - var options = {}; - - // Make sure the widget has a selector set - if ( !selector ) { - return; - } - if ( widget === "controlgroupLabel" ) { - labels = that.element.find( selector ); - labels.each( function() { - var element = $( this ); - - if ( element.children( ".ui-controlgroup-label-contents" ).length ) { - return; - } - element.contents() - .wrapAll( "<span class='ui-controlgroup-label-contents'></span>" ); - } ); - that._addClass( labels, null, "ui-widget ui-widget-content ui-state-default" ); - childWidgets = childWidgets.concat( labels.get() ); - return; - } + var dateFormat = this._get( inst, "dateFormat" ), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate( inst ), + date = defaultDate, + settings = this._getFormatConfig( inst ); - // Make sure the widget actually exists - if ( !$.fn[ widget ] ) { - return; - } + try { + date = this.parseDate( dateFormat, dates, settings ) || defaultDate; + } catch ( event ) { + dates = ( noDefault ? "" : dates ); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = ( dates ? date.getDate() : 0 ); + inst.currentMonth = ( dates ? date.getMonth() : 0 ); + inst.currentYear = ( dates ? date.getFullYear() : 0 ); + this._adjustInstDate( inst ); + }, - // We assume everything is in the middle to start because we can't determine - // first / last elements until all enhancments are done. - if ( that[ "_" + widget + "Options" ] ) { - options = that[ "_" + widget + "Options" ]( "middle" ); - } else { - options = { classes: {} }; - } + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function( inst ) { + return this._restrictMinMax( inst, + this._determineDate( inst, this._get( inst, "defaultDate" ), new Date() ) ); + }, - // Find instances of this widget inside controlgroup and init them - that.element - .find( selector ) - .each( function() { - var element = $( this ); - var instance = element[ widget ]( "instance" ); + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function( inst, date, defaultDate ) { + var offsetNumeric = function( offset ) { + var date = new Date(); + date.setDate( date.getDate() + offset ); + return date; + }, + offsetString = function( offset ) { + try { + return $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ), + offset, $.datepicker._getFormatConfig( inst ) ); + } catch ( e ) { - // We need to clone the default options for this type of widget to avoid - // polluting the variable options which has a wider scope than a single widget. - var instanceOptions = $.widget.extend( {}, options ); + // Ignore + } - // If the button is the child of a spinner ignore it - // TODO: Find a more generic solution - if ( widget === "button" && element.parent( ".ui-spinner" ).length ) { - return; - } + var date = ( offset.toLowerCase().match( /^c/ ) ? + $.datepicker._getDate( inst ) : null ) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec( offset ); - // Create the widget if it doesn't exist - if ( !instance ) { - instance = element[ widget ]()[ widget ]( "instance" ); - } - if ( instance ) { - instanceOptions.classes = - that._resolveClassesValues( instanceOptions.classes, instance ); + while ( matches ) { + switch ( matches[ 2 ] || "d" ) { + case "d" : case "D" : + day += parseInt( matches[ 1 ], 10 ); break; + case "w" : case "W" : + day += parseInt( matches[ 1 ], 10 ) * 7; break; + case "m" : case "M" : + month += parseInt( matches[ 1 ], 10 ); + day = Math.min( day, $.datepicker._getDaysInMonth( year, month ) ); + break; + case "y": case "Y" : + year += parseInt( matches[ 1 ], 10 ); + day = Math.min( day, $.datepicker._getDaysInMonth( year, month ) ); + break; } - element[ widget ]( instanceOptions ); - - // Store an instance of the controlgroup to be able to reference - // from the outermost element for changing options and refresh - var widgetElement = element[ widget ]( "widget" ); - $.data( widgetElement[ 0 ], "ui-controlgroup-data", - instance ? instance : element[ widget ]( "instance" ) ); - - childWidgets.push( widgetElement[ 0 ] ); - } ); - } ); - - this.childWidgets = $( $.uniqueSort( childWidgets ) ); - this._addClass( this.childWidgets, "ui-controlgroup-item" ); - }, - - _callChildMethod: function( method ) { - this.childWidgets.each( function() { - var element = $( this ), - data = element.data( "ui-controlgroup-data" ); - if ( data && data[ method ] ) { - data[ method ](); - } - } ); - }, - - _updateCornerClass: function( element, position ) { - var remove = "ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-corner-all"; - var add = this._buildSimpleOptions( position, "label" ).classes.label; + matches = pattern.exec( offset ); + } + return new Date( year, month, day ); + }, + newDate = ( date == null || date === "" ? defaultDate : ( typeof date === "string" ? offsetString( date ) : + ( typeof date === "number" ? ( isNaN( date ) ? defaultDate : offsetNumeric( date ) ) : new Date( date.getTime() ) ) ) ); - this._removeClass( element, null, remove ); - this._addClass( element, null, add ); + newDate = ( newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate ); + if ( newDate ) { + newDate.setHours( 0 ); + newDate.setMinutes( 0 ); + newDate.setSeconds( 0 ); + newDate.setMilliseconds( 0 ); + } + return this._daylightSavingAdjust( newDate ); }, - _buildSimpleOptions: function( position, key ) { - var direction = this.options.direction === "vertical"; - var result = { - classes: {} - }; - result.classes[ key ] = { - "middle": "", - "first": "ui-corner-" + ( direction ? "top" : "left" ), - "last": "ui-corner-" + ( direction ? "bottom" : "right" ), - "only": "ui-corner-all" - }[ position ]; - - return result; + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function( date ) { + if ( !date ) { + return null; + } + date.setHours( date.getHours() > 12 ? date.getHours() + 2 : 0 ); + return date; }, - _spinnerOptions: function( position ) { - var options = this._buildSimpleOptions( position, "ui-spinner" ); - - options.classes[ "ui-spinner-up" ] = ""; - options.classes[ "ui-spinner-down" ] = ""; - - return options; - }, + /* Set the date(s) directly. */ + _setDate: function( inst, date, noChange ) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax( inst, this._determineDate( inst, date, new Date() ) ); - _buttonOptions: function( position ) { - return this._buildSimpleOptions( position, "ui-button" ); + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ( ( origMonth !== inst.selectedMonth || origYear !== inst.selectedYear ) && !noChange ) { + this._notifyChange( inst ); + } + this._adjustInstDate( inst ); + if ( inst.input ) { + inst.input.val( clear ? "" : this._formatDate( inst ) ); + } }, - _checkboxradioOptions: function( position ) { - return this._buildSimpleOptions( position, "ui-checkboxradio-label" ); + /* Retrieve the date(s) directly. */ + _getDate: function( inst ) { + var startDate = ( !inst.currentYear || ( inst.input && inst.input.val() === "" ) ? null : + this._daylightSavingAdjust( new Date( + inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); + return startDate; }, - _selectmenuOptions: function( position ) { - var direction = this.options.direction === "vertical"; - return { - width: direction ? "auto" : false, - classes: { - middle: { - "ui-selectmenu-button-open": "", - "ui-selectmenu-button-closed": "" - }, - first: { - "ui-selectmenu-button-open": "ui-corner-" + ( direction ? "top" : "tl" ), - "ui-selectmenu-button-closed": "ui-corner-" + ( direction ? "top" : "left" ) - }, - last: { - "ui-selectmenu-button-open": direction ? "" : "ui-corner-tr", - "ui-selectmenu-button-closed": "ui-corner-" + ( direction ? "bottom" : "right" ) + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function( inst ) { + var stepMonths = this._get( inst, "stepMonths" ), + id = "#" + inst.id.replace( /\\\\/g, "\\" ); + inst.dpDiv.find( "[data-handler]" ).map( function() { + var handler = { + prev: function() { + $.datepicker._adjustDate( id, -stepMonths, "M" ); }, - only: { - "ui-selectmenu-button-open": "ui-corner-top", - "ui-selectmenu-button-closed": "ui-corner-all" + next: function() { + $.datepicker._adjustDate( id, +stepMonths, "M" ); + }, + hide: function() { + $.datepicker._hideDatepicker(); + }, + today: function() { + $.datepicker._gotoToday( id ); + }, + selectDay: function() { + $.datepicker._selectDay( id, +this.getAttribute( "data-month" ), +this.getAttribute( "data-year" ), this ); + return false; + }, + selectMonth: function() { + $.datepicker._selectMonthYear( id, this, "M" ); + return false; + }, + selectYear: function() { + $.datepicker._selectMonthYear( id, this, "Y" ); + return false; } - - }[ position ] - }; - }, - - _resolveClassesValues: function( classes, instance ) { - var result = {}; - $.each( classes, function( key ) { - var current = instance.options.classes[ key ] || ""; - current = String.prototype.trim.call( current.replace( controlgroupCornerRegex, "" ) ); - result[ key ] = ( current + " " + classes[ key ] ).replace( /\s+/g, " " ); + }; + $( this ).on( this.getAttribute( "data-event" ), handler[ this.getAttribute( "data-handler" ) ] ); } ); - return result; }, - _setOption: function( key, value ) { - if ( key === "direction" ) { - this._removeClass( "ui-controlgroup-" + this.options.direction ); - } + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function( inst ) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date( tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() ) ), // clear time + isRTL = this._get( inst, "isRTL" ), + showButtonPanel = this._get( inst, "showButtonPanel" ), + hideIfNoPrevNext = this._get( inst, "hideIfNoPrevNext" ), + navigationAsDateFormat = this._get( inst, "navigationAsDateFormat" ), + numMonths = this._getNumberOfMonths( inst ), + showCurrentAtPos = this._get( inst, "showCurrentAtPos" ), + stepMonths = this._get( inst, "stepMonths" ), + isMultiMonth = ( numMonths[ 0 ] !== 1 || numMonths[ 1 ] !== 1 ), + currentDate = this._daylightSavingAdjust( ( !inst.currentDay ? new Date( 9999, 9, 9 ) : + new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ), + minDate = this._getMinMaxDate( inst, "min" ), + maxDate = this._getMinMaxDate( inst, "max" ), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; - this._super( key, value ); - if ( key === "disabled" ) { - this._callChildMethod( value ? "disable" : "enable" ); - return; + if ( drawMonth < 0 ) { + drawMonth += 12; + drawYear--; } + if ( maxDate ) { + maxDraw = this._daylightSavingAdjust( new Date( maxDate.getFullYear(), + maxDate.getMonth() - ( numMonths[ 0 ] * numMonths[ 1 ] ) + 1, maxDate.getDate() ) ); + maxDraw = ( minDate && maxDraw < minDate ? minDate : maxDraw ); + while ( this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 ) ) > maxDraw ) { + drawMonth--; + if ( drawMonth < 0 ) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; - this.refresh(); - }, - - refresh: function() { - var children, - that = this; - - this._addClass( "ui-controlgroup ui-controlgroup-" + this.options.direction ); + prevText = this._get( inst, "prevText" ); + prevText = ( !navigationAsDateFormat ? prevText : this.formatDate( prevText, + this._daylightSavingAdjust( new Date( drawYear, drawMonth - stepMonths, 1 ) ), + this._getFormatConfig( inst ) ) ); - if ( this.options.direction === "horizontal" ) { - this._addClass( null, "ui-helper-clearfix" ); + if ( this._canAdjustMonth( inst, -1, drawYear, drawMonth ) ) { + prev = $( "<a>" ) + .attr( { + "class": "ui-datepicker-prev ui-corner-all", + "data-handler": "prev", + "data-event": "click", + title: prevText + } ) + .append( + $( "<span>" ) + .addClass( "ui-icon ui-icon-circle-triangle-" + + ( isRTL ? "e" : "w" ) ) + .text( prevText ) + )[ 0 ].outerHTML; + } else if ( hideIfNoPrevNext ) { + prev = ""; + } else { + prev = $( "<a>" ) + .attr( { + "class": "ui-datepicker-prev ui-corner-all ui-state-disabled", + title: prevText + } ) + .append( + $( "<span>" ) + .addClass( "ui-icon ui-icon-circle-triangle-" + + ( isRTL ? "e" : "w" ) ) + .text( prevText ) + )[ 0 ].outerHTML; } - this._initWidgets(); - children = this.childWidgets; + nextText = this._get( inst, "nextText" ); + nextText = ( !navigationAsDateFormat ? nextText : this.formatDate( nextText, + this._daylightSavingAdjust( new Date( drawYear, drawMonth + stepMonths, 1 ) ), + this._getFormatConfig( inst ) ) ); - // We filter here because we need to track all childWidgets not just the visible ones - if ( this.options.onlyVisible ) { - children = children.filter( ":visible" ); + if ( this._canAdjustMonth( inst, +1, drawYear, drawMonth ) ) { + next = $( "<a>" ) + .attr( { + "class": "ui-datepicker-next ui-corner-all", + "data-handler": "next", + "data-event": "click", + title: nextText + } ) + .append( + $( "<span>" ) + .addClass( "ui-icon ui-icon-circle-triangle-" + + ( isRTL ? "w" : "e" ) ) + .text( nextText ) + )[ 0 ].outerHTML; + } else if ( hideIfNoPrevNext ) { + next = ""; + } else { + next = $( "<a>" ) + .attr( { + "class": "ui-datepicker-next ui-corner-all ui-state-disabled", + title: nextText + } ) + .append( + $( "<span>" ) + .attr( "class", "ui-icon ui-icon-circle-triangle-" + + ( isRTL ? "w" : "e" ) ) + .text( nextText ) + )[ 0 ].outerHTML; } - if ( children.length ) { - - // We do this last because we need to make sure all enhancment is done - // before determining first and last - $.each( [ "first", "last" ], function( index, value ) { - var instance = children[ value ]().data( "ui-controlgroup-data" ); + currentText = this._get( inst, "currentText" ); + gotoDate = ( this._get( inst, "gotoCurrent" ) && inst.currentDay ? currentDate : today ); + currentText = ( !navigationAsDateFormat ? currentText : + this.formatDate( currentText, gotoDate, this._getFormatConfig( inst ) ) ); - if ( instance && that[ "_" + instance.widgetName + "Options" ] ) { - var options = that[ "_" + instance.widgetName + "Options" ]( - children.length === 1 ? "only" : value - ); - options.classes = that._resolveClassesValues( options.classes, instance ); - instance.element[ instance.widgetName ]( options ); - } else { - that._updateCornerClass( children[ value ](), value ); - } - } ); + controls = ""; + if ( !inst.inline ) { + controls = $( "<button>" ) + .attr( { + type: "button", + "class": "ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all", + "data-handler": "hide", + "data-event": "click" + } ) + .text( this._get( inst, "closeText" ) )[ 0 ].outerHTML; + } - // Finally call the refresh method on each of the child widgets. - this._callChildMethod( "refresh" ); + buttonPanel = ""; + if ( showButtonPanel ) { + buttonPanel = $( "<div class='ui-datepicker-buttonpane ui-widget-content'>" ) + .append( isRTL ? controls : "" ) + .append( this._isInRange( inst, gotoDate ) ? + $( "<button>" ) + .attr( { + type: "button", + "class": "ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all", + "data-handler": "today", + "data-event": "click" + } ) + .text( currentText ) : + "" ) + .append( isRTL ? "" : controls )[ 0 ].outerHTML; } - } -} ); -/*! - * jQuery UI Checkboxradio 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + firstDay = parseInt( this._get( inst, "firstDay" ), 10 ); + firstDay = ( isNaN( firstDay ) ? 0 : firstDay ); -//>>label: Checkboxradio -//>>group: Widgets -//>>description: Enhances a form with multiple themeable checkboxes or radio buttons. -//>>docs: http://api.jqueryui.com/checkboxradio/ -//>>demos: http://jqueryui.com/checkboxradio/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/button.css -//>>css.structure: ../../themes/base/checkboxradio.css -//>>css.theme: ../../themes/base/theme.css + showWeek = this._get( inst, "showWeek" ); + dayNames = this._get( inst, "dayNames" ); + dayNamesMin = this._get( inst, "dayNamesMin" ); + monthNames = this._get( inst, "monthNames" ); + monthNamesShort = this._get( inst, "monthNamesShort" ); + beforeShowDay = this._get( inst, "beforeShowDay" ); + showOtherMonths = this._get( inst, "showOtherMonths" ); + selectOtherMonths = this._get( inst, "selectOtherMonths" ); + defaultDate = this._getDefaultDate( inst ); + html = ""; + for ( row = 0; row < numMonths[ 0 ]; row++ ) { + group = ""; + this.maxRows = 4; + for ( col = 0; col < numMonths[ 1 ]; col++ ) { + selectedDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, inst.selectedDay ) ); + cornerClass = " ui-corner-all"; + calender = ""; + if ( isMultiMonth ) { + calender += "<div class='ui-datepicker-group"; + if ( numMonths[ 1 ] > 1 ) { + switch ( col ) { + case 0: calender += " ui-datepicker-group-first"; + cornerClass = " ui-corner-" + ( isRTL ? "right" : "left" ); break; + case numMonths[ 1 ] - 1: calender += " ui-datepicker-group-last"; + cornerClass = " ui-corner-" + ( isRTL ? "left" : "right" ); break; + default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break; + } + } + calender += "'>"; + } + calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + + ( /all|left/.test( cornerClass ) && row === 0 ? ( isRTL ? next : prev ) : "" ) + + ( /all|right/.test( cornerClass ) && row === 0 ? ( isRTL ? prev : next ) : "" ) + + this._generateMonthYearHeader( inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort ) + // draw month headers + "</div><table class='ui-datepicker-calendar'><thead>" + + "<tr>"; + thead = ( showWeek ? "<th class='ui-datepicker-week-col'>" + this._get( inst, "weekHeader" ) + "</th>" : "" ); + for ( dow = 0; dow < 7; dow++ ) { // days of the week + day = ( dow + firstDay ) % 7; + thead += "<th scope='col'" + ( ( dow + firstDay + 6 ) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "" ) + ">" + + "<span title='" + dayNames[ day ] + "'>" + dayNamesMin[ day ] + "</span></th>"; + } + calender += thead + "</tr></thead><tbody>"; + daysInMonth = this._getDaysInMonth( drawYear, drawMonth ); + if ( drawYear === inst.selectedYear && drawMonth === inst.selectedMonth ) { + inst.selectedDay = Math.min( inst.selectedDay, daysInMonth ); + } + leadDays = ( this._getFirstDayOfMonth( drawYear, drawMonth ) - firstDay + 7 ) % 7; + curRows = Math.ceil( ( leadDays + daysInMonth ) / 7 ); // calculate the number of rows to generate + numRows = ( isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows ); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 - leadDays ) ); + for ( dRow = 0; dRow < numRows; dRow++ ) { // create date picker rows + calender += "<tr>"; + tbody = ( !showWeek ? "" : "<td class='ui-datepicker-week-col'>" + + this._get( inst, "calculateWeek" )( printDate ) + "</td>" ); + for ( dow = 0; dow < 7; dow++ ) { // create date picker days + daySettings = ( beforeShowDay ? + beforeShowDay.apply( ( inst.input ? inst.input[ 0 ] : null ), [ printDate ] ) : [ true, "" ] ); + otherMonth = ( printDate.getMonth() !== drawMonth ); + unselectable = ( otherMonth && !selectOtherMonths ) || !daySettings[ 0 ] || + ( minDate && printDate < minDate ) || ( maxDate && printDate > maxDate ); + tbody += "<td class='" + + ( ( dow + firstDay + 6 ) % 7 >= 5 ? " ui-datepicker-week-end" : "" ) + // highlight weekends + ( otherMonth ? " ui-datepicker-other-month" : "" ) + // highlight days from other months + ( ( printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent ) || // user pressed key + ( defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime() ) ? -$.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { - version: "1.13.1", - options: { - disabled: null, - label: null, - icon: true, - classes: { - "ui-checkboxradio-label": "ui-corner-all", - "ui-checkboxradio-icon": "ui-corner-all" + // or defaultDate is current printedDate and defaultDate is selectedDate + " " + this._dayOverClass : "" ) + // highlight selected day + ( unselectable ? " " + this._unselectableClass + " ui-state-disabled" : "" ) + // highlight unselectable days + ( otherMonth && !showOtherMonths ? "" : " " + daySettings[ 1 ] + // highlight custom dates + ( printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "" ) + // highlight selected day + ( printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "" ) ) + "'" + // highlight today (if different) + ( ( !otherMonth || showOtherMonths ) && daySettings[ 2 ] ? " title='" + daySettings[ 2 ].replace( /'/g, "'" ) + "'" : "" ) + // cell title + ( unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'" ) + ">" + // actions + ( otherMonth && !showOtherMonths ? " " : // display for other months + ( unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + + ( printDate.getTime() === today.getTime() ? " ui-state-highlight" : "" ) + + ( printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "" ) + // highlight selected day + ( otherMonth ? " ui-priority-secondary" : "" ) + // distinguish dates from other months + "' href='#' aria-current='" + ( printDate.getTime() === currentDate.getTime() ? "true" : "false" ) + // mark date as selected for screen reader + "' data-date='" + printDate.getDate() + // store date as data + "'>" + printDate.getDate() + "</a>" ) ) + "</td>"; // display selectable date + printDate.setDate( printDate.getDate() + 1 ); + printDate = this._daylightSavingAdjust( printDate ); + } + calender += tbody + "</tr>"; + } + drawMonth++; + if ( drawMonth > 11 ) { + drawMonth = 0; + drawYear++; + } + calender += "</tbody></table>" + ( isMultiMonth ? "</div>" + + ( ( numMonths[ 0 ] > 0 && col === numMonths[ 1 ] - 1 ) ? "<div class='ui-datepicker-row-break'></div>" : "" ) : "" ); + group += calender; + } + html += group; } + html += buttonPanel; + inst._keyEvent = false; + return html; }, - _getCreateOptions: function() { - var disabled, labels; - var that = this; - var options = this._super() || {}; - - // We read the type here, because it makes more sense to throw a element type error first, - // rather then the error for lack of a label. Often if its the wrong type, it - // won't have a label (e.g. calling on a div, btn, etc) - this._readType(); + /* Generate the month and year header. */ + _generateMonthYearHeader: function( inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort ) { - labels = this.element.labels(); + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get( inst, "changeMonth" ), + changeYear = this._get( inst, "changeYear" ), + showMonthAfterYear = this._get( inst, "showMonthAfterYear" ), + selectMonthLabel = this._get( inst, "selectMonthLabel" ), + selectYearLabel = this._get( inst, "selectYearLabel" ), + html = "<div class='ui-datepicker-title'>", + monthHtml = ""; - // If there are multiple labels, use the last one - this.label = $( labels[ labels.length - 1 ] ); - if ( !this.label.length ) { - $.error( "No label found for checkboxradio widget" ); + // Month selection + if ( secondary || !changeMonth ) { + monthHtml += "<span class='ui-datepicker-month'>" + monthNames[ drawMonth ] + "</span>"; + } else { + inMinYear = ( minDate && minDate.getFullYear() === drawYear ); + inMaxYear = ( maxDate && maxDate.getFullYear() === drawYear ); + monthHtml += "<select class='ui-datepicker-month' aria-label='" + selectMonthLabel + "' data-handler='selectMonth' data-event='change'>"; + for ( month = 0; month < 12; month++ ) { + if ( ( !inMinYear || month >= minDate.getMonth() ) && ( !inMaxYear || month <= maxDate.getMonth() ) ) { + monthHtml += "<option value='" + month + "'" + + ( month === drawMonth ? " selected='selected'" : "" ) + + ">" + monthNamesShort[ month ] + "</option>"; + } + } + monthHtml += "</select>"; } - this.originalLabel = ""; + if ( !showMonthAfterYear ) { + html += monthHtml + ( secondary || !( changeMonth && changeYear ) ? " " : "" ); + } - // We need to get the label text but this may also need to make sure it does not contain the - // input itself. - this.label.contents().not( this.element[ 0 ] ).each( function() { + // Year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ""; + if ( secondary || !changeYear ) { + html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; + } else { - // The label contents could be text, html, or a mix. We concat each element to get a - // string representation of the label, without the input as part of it. - that.originalLabel += this.nodeType === 3 ? $( this ).text() : this.outerHTML; - } ); + // determine range of years to display + years = this._get( inst, "yearRange" ).split( ":" ); + thisYear = new Date().getFullYear(); + determineYear = function( value ) { + var year = ( value.match( /c[+\-].*/ ) ? drawYear + parseInt( value.substring( 1 ), 10 ) : + ( value.match( /[+\-].*/ ) ? thisYear + parseInt( value, 10 ) : + parseInt( value, 10 ) ) ); + return ( isNaN( year ) ? thisYear : year ); + }; + year = determineYear( years[ 0 ] ); + endYear = Math.max( year, determineYear( years[ 1 ] || "" ) ); + year = ( minDate ? Math.max( year, minDate.getFullYear() ) : year ); + endYear = ( maxDate ? Math.min( endYear, maxDate.getFullYear() ) : endYear ); + inst.yearshtml += "<select class='ui-datepicker-year' aria-label='" + selectYearLabel + "' data-handler='selectYear' data-event='change'>"; + for ( ; year <= endYear; year++ ) { + inst.yearshtml += "<option value='" + year + "'" + + ( year === drawYear ? " selected='selected'" : "" ) + + ">" + year + "</option>"; + } + inst.yearshtml += "</select>"; - // Set the label option if we found label text - if ( this.originalLabel ) { - options.label = this.originalLabel; + html += inst.yearshtml; + inst.yearshtml = null; + } } - disabled = this.element[ 0 ].disabled; - if ( disabled != null ) { - options.disabled = disabled; + html += this._get( inst, "yearSuffix" ); + if ( showMonthAfterYear ) { + html += ( secondary || !( changeMonth && changeYear ) ? " " : "" ) + monthHtml; } - return options; + html += "</div>"; // Close datepicker_header + return html; }, - _create: function() { - var checked = this.element[ 0 ].checked; - - this._bindFormResetHandler(); + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function( inst, offset, period ) { + var year = inst.selectedYear + ( period === "Y" ? offset : 0 ), + month = inst.selectedMonth + ( period === "M" ? offset : 0 ), + day = Math.min( inst.selectedDay, this._getDaysInMonth( year, month ) ) + ( period === "D" ? offset : 0 ), + date = this._restrictMinMax( inst, this._daylightSavingAdjust( new Date( year, month, day ) ) ); - if ( this.options.disabled == null ) { - this.options.disabled = this.element[ 0 ].disabled; + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if ( period === "M" || period === "Y" ) { + this._notifyChange( inst ); } + }, - this._setOption( "disabled", this.options.disabled ); - this._addClass( "ui-checkboxradio", "ui-helper-hidden-accessible" ); - this._addClass( this.label, "ui-checkboxradio-label", "ui-button ui-widget" ); - - if ( this.type === "radio" ) { - this._addClass( this.label, "ui-checkboxradio-radio-label" ); - } + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function( inst, date ) { + var minDate = this._getMinMaxDate( inst, "min" ), + maxDate = this._getMinMaxDate( inst, "max" ), + newDate = ( minDate && date < minDate ? minDate : date ); + return ( maxDate && newDate > maxDate ? maxDate : newDate ); + }, - if ( this.options.label && this.options.label !== this.originalLabel ) { - this._updateLabel(); - } else if ( this.originalLabel ) { - this.options.label = this.originalLabel; + /* Notify change of month/year. */ + _notifyChange: function( inst ) { + var onChange = this._get( inst, "onChangeMonthYear" ); + if ( onChange ) { + onChange.apply( ( inst.input ? inst.input[ 0 ] : null ), + [ inst.selectedYear, inst.selectedMonth + 1, inst ] ); } + }, - this._enhance(); + /* Determine the number of months to show. */ + _getNumberOfMonths: function( inst ) { + var numMonths = this._get( inst, "numberOfMonths" ); + return ( numMonths == null ? [ 1, 1 ] : ( typeof numMonths === "number" ? [ 1, numMonths ] : numMonths ) ); + }, - if ( checked ) { - this._addClass( this.label, "ui-checkboxradio-checked", "ui-state-active" ); - } + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function( inst, minMax ) { + return this._determineDate( inst, this._get( inst, minMax + "Date" ), null ); + }, - this._on( { - change: "_toggleClasses", - focus: function() { - this._addClass( this.label, null, "ui-state-focus ui-visual-focus" ); - }, - blur: function() { - this._removeClass( this.label, null, "ui-state-focus ui-visual-focus" ); - } - } ); + /* Find the number of days in a given month. */ + _getDaysInMonth: function( year, month ) { + return 32 - this._daylightSavingAdjust( new Date( year, month, 32 ) ).getDate(); }, - _readType: function() { - var nodeName = this.element[ 0 ].nodeName.toLowerCase(); - this.type = this.element[ 0 ].type; - if ( nodeName !== "input" || !/radio|checkbox/.test( this.type ) ) { - $.error( "Can't create checkboxradio on element.nodeName=" + nodeName + - " and element.type=" + this.type ); - } - }, - - // Support jQuery Mobile enhanced option - _enhance: function() { - this._updateIcon( this.element[ 0 ].checked ); - }, - - widget: function() { - return this.label; + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function( year, month ) { + return new Date( year, month, 1 ).getDay(); }, - _getRadioGroup: function() { - var group; - var name = this.element[ 0 ].name; - var nameSelector = "input[name='" + $.escapeSelector( name ) + "']"; + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function( inst, offset, curYear, curMonth ) { + var numMonths = this._getNumberOfMonths( inst ), + date = this._daylightSavingAdjust( new Date( curYear, + curMonth + ( offset < 0 ? offset : numMonths[ 0 ] * numMonths[ 1 ] ), 1 ) ); - if ( !name ) { - return $( [] ); + if ( offset < 0 ) { + date.setDate( this._getDaysInMonth( date.getFullYear(), date.getMonth() ) ); } + return this._isInRange( inst, date ); + }, - if ( this.form.length ) { - group = $( this.form[ 0 ].elements ).filter( nameSelector ); - } else { - - // Not inside a form, check all inputs that also are not inside a form - group = $( nameSelector ).filter( function() { - return $( this )._form().length === 0; - } ); - } + /* Is the given date in the accepted range? */ + _isInRange: function( inst, date ) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate( inst, "min" ), + maxDate = this._getMinMaxDate( inst, "max" ), + minYear = null, + maxYear = null, + years = this._get( inst, "yearRange" ); + if ( years ) { + yearSplit = years.split( ":" ); + currentYear = new Date().getFullYear(); + minYear = parseInt( yearSplit[ 0 ], 10 ); + maxYear = parseInt( yearSplit[ 1 ], 10 ); + if ( yearSplit[ 0 ].match( /[+\-].*/ ) ) { + minYear += currentYear; + } + if ( yearSplit[ 1 ].match( /[+\-].*/ ) ) { + maxYear += currentYear; + } + } - return group.not( this.element ); + return ( ( !minDate || date.getTime() >= minDate.getTime() ) && + ( !maxDate || date.getTime() <= maxDate.getTime() ) && + ( !minYear || date.getFullYear() >= minYear ) && + ( !maxYear || date.getFullYear() <= maxYear ) ); }, - _toggleClasses: function() { - var checked = this.element[ 0 ].checked; - this._toggleClass( this.label, "ui-checkboxradio-checked", "ui-state-active", checked ); + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function( inst ) { + var shortYearCutoff = this._get( inst, "shortYearCutoff" ); + shortYearCutoff = ( typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt( shortYearCutoff, 10 ) ); + return { shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get( inst, "dayNamesShort" ), dayNames: this._get( inst, "dayNames" ), + monthNamesShort: this._get( inst, "monthNamesShort" ), monthNames: this._get( inst, "monthNames" ) }; + }, - if ( this.options.icon && this.type === "checkbox" ) { - this._toggleClass( this.icon, null, "ui-icon-check ui-state-checked", checked ) - ._toggleClass( this.icon, null, "ui-icon-blank", !checked ); + /* Format the given date for display. */ + _formatDate: function( inst, day, month, year ) { + if ( !day ) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; } + var date = ( day ? ( typeof day === "object" ? day : + this._daylightSavingAdjust( new Date( year, month, day ) ) ) : + this._daylightSavingAdjust( new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); + return this.formatDate( this._get( inst, "dateFormat" ), date, this._getFormatConfig( inst ) ); + } +} ); - if ( this.type === "radio" ) { - this._getRadioGroup() - .each( function() { - var instance = $( this ).checkboxradio( "instance" ); +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function datepicker_bindHover( dpDiv ) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.on( "mouseout", selector, function() { + $( this ).removeClass( "ui-state-hover" ); + if ( this.className.indexOf( "ui-datepicker-prev" ) !== -1 ) { + $( this ).removeClass( "ui-datepicker-prev-hover" ); + } + if ( this.className.indexOf( "ui-datepicker-next" ) !== -1 ) { + $( this ).removeClass( "ui-datepicker-next-hover" ); + } + } ) + .on( "mouseover", selector, datepicker_handleMouseover ); +} - if ( instance ) { - instance._removeClass( instance.label, - "ui-checkboxradio-checked", "ui-state-active" ); - } - } ); +function datepicker_handleMouseover() { + if ( !$.datepicker._isDisabledDatepicker( datepicker_instActive.inline ? datepicker_instActive.dpDiv.parent()[ 0 ] : datepicker_instActive.input[ 0 ] ) ) { + $( this ).parents( ".ui-datepicker-calendar" ).find( "a" ).removeClass( "ui-state-hover" ); + $( this ).addClass( "ui-state-hover" ); + if ( this.className.indexOf( "ui-datepicker-prev" ) !== -1 ) { + $( this ).addClass( "ui-datepicker-prev-hover" ); } - }, - - _destroy: function() { - this._unbindFormResetHandler(); + if ( this.className.indexOf( "ui-datepicker-next" ) !== -1 ) { + $( this ).addClass( "ui-datepicker-next-hover" ); + } + } +} - if ( this.icon ) { - this.icon.remove(); - this.iconSpace.remove(); +/* jQuery extend now ignores nulls! */ +function datepicker_extendRemove( target, props ) { + $.extend( target, props ); + for ( var name in props ) { + if ( props[ name ] == null ) { + target[ name ] = props[ name ]; } - }, + } + return target; +} - _setOption: function( key, value ) { +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function( options ) { - // We don't allow the value to be set to nothing - if ( key === "label" && !value ) { - return; - } + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } - this._super( key, value ); + /* Initialise the date picker. */ + if ( !$.datepicker.initialized ) { + $( document ).on( "mousedown", $.datepicker._checkExternalClick ); + $.datepicker.initialized = true; + } - if ( key === "disabled" ) { - this._toggleClass( this.label, null, "ui-state-disabled", value ); - this.element[ 0 ].disabled = value; + /* Append datepicker main container to body if not exist. */ + if ( $( "#" + $.datepicker._mainDivId ).length === 0 ) { + $( "body" ).append( $.datepicker.dpDiv ); + } - // Don't refresh when setting disabled - return; + var otherArgs = Array.prototype.slice.call( arguments, 1 ); + if ( typeof options === "string" && ( options === "isDisabled" || options === "getDate" || options === "widget" ) ) { + return $.datepicker[ "_" + options + "Datepicker" ]. + apply( $.datepicker, [ this[ 0 ] ].concat( otherArgs ) ); + } + if ( options === "option" && arguments.length === 2 && typeof arguments[ 1 ] === "string" ) { + return $.datepicker[ "_" + options + "Datepicker" ]. + apply( $.datepicker, [ this[ 0 ] ].concat( otherArgs ) ); + } + return this.each( function() { + if ( typeof options === "string" ) { + $.datepicker[ "_" + options + "Datepicker" ] + .apply( $.datepicker, [ this ].concat( otherArgs ) ); + } else { + $.datepicker._attachDatepicker( this, options ); } - this.refresh(); - }, + } ); +}; - _updateIcon: function( checked ) { - var toAdd = "ui-icon ui-icon-background "; +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.13.2"; - if ( this.options.icon ) { - if ( !this.icon ) { - this.icon = $( "<span>" ); - this.iconSpace = $( "<span> </span>" ); - this._addClass( this.iconSpace, "ui-checkboxradio-icon-space" ); - } +var widgetsDatepicker = $.datepicker; - if ( this.type === "checkbox" ) { - toAdd += checked ? "ui-icon-check ui-state-checked" : "ui-icon-blank"; - this._removeClass( this.icon, null, checked ? "ui-icon-blank" : "ui-icon-check" ); - } else { - toAdd += "ui-icon-blank"; - } - this._addClass( this.icon, "ui-checkboxradio-icon", toAdd ); - if ( !checked ) { - this._removeClass( this.icon, null, "ui-icon-check ui-state-checked" ); - } - this.icon.prependTo( this.label ).after( this.iconSpace ); - } else if ( this.icon !== undefined ) { - this.icon.remove(); - this.iconSpace.remove(); - delete this.icon; - } - }, - _updateLabel: function() { - // Remove the contents of the label ( minus the icon, icon space, and input ) - var contents = this.label.contents().not( this.element[ 0 ] ); - if ( this.icon ) { - contents = contents.not( this.icon[ 0 ] ); - } - if ( this.iconSpace ) { - contents = contents.not( this.iconSpace[ 0 ] ); - } - contents.remove(); +// This file is deprecated +var ie = $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - this.label.append( this.options.label ); - }, +/*! + * jQuery UI Mouse 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - refresh: function() { - var checked = this.element[ 0 ].checked, - isDisabled = this.element[ 0 ].disabled; +//>>label: Mouse +//>>group: Widgets +//>>description: Abstracts mouse-based interactions to assist in creating certain widgets. +//>>docs: http://api.jqueryui.com/mouse/ - this._updateIcon( checked ); - this._toggleClass( this.label, "ui-checkboxradio-checked", "ui-state-active", checked ); - if ( this.options.label !== null ) { - this._updateLabel(); - } - if ( isDisabled !== this.options.disabled ) { - this._setOptions( { "disabled": isDisabled } ); - } - } +var mouseHandled = false; +$( document ).on( "mouseup", function() { + mouseHandled = false; +} ); -} ] ); +var widgetsMouse = $.widget( "ui.mouse", { + version: "1.13.2", + options: { + cancel: "input, textarea, button, select, option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; -var widgetsCheckboxradio = $.ui.checkboxradio; + this.element + .on( "mousedown." + this.widgetName, function( event ) { + return that._mouseDown( event ); + } ) + .on( "click." + this.widgetName, function( event ) { + if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, that.widgetName + ".preventClickEvent" ); + event.stopImmediatePropagation(); + return false; + } + } ); + this.started = false; + }, -/*! - * jQuery UI Button 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.off( "." + this.widgetName ); + if ( this._mouseMoveDelegate ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + } + }, -//>>label: Button -//>>group: Widgets -//>>description: Enhances a form with themeable buttons. -//>>docs: http://api.jqueryui.com/button/ -//>>demos: http://jqueryui.com/button/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/button.css -//>>css.theme: ../../themes/base/theme.css + _mouseDown: function( event ) { + // don't let more than one widget handle mouseStart + if ( mouseHandled ) { + return; + } -$.widget( "ui.button", { - version: "1.13.1", - defaultElement: "<button>", - options: { - classes: { - "ui-button": "ui-corner-all" - }, - disabled: null, - icon: null, - iconPosition: "beginning", - label: null, - showLabel: true - }, + this._mouseMoved = false; - _getCreateOptions: function() { - var disabled, + // We may have missed mouseup (out of window) + if ( this._mouseStarted ) { + this._mouseUp( event ); + } - // This is to support cases like in jQuery Mobile where the base widget does have - // an implementation of _getCreateOptions - options = this._super() || {}; + this._mouseDownEvent = event; - this.isInput = this.element.is( "input" ); + var that = this, + btnIsLeft = ( event.which === 1 ), - disabled = this.element[ 0 ].disabled; - if ( disabled != null ) { - options.disabled = disabled; + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = ( typeof this.options.cancel === "string" && event.target.nodeName ? + $( event.target ).closest( this.options.cancel ).length : false ); + if ( !btnIsLeft || elIsCancel || !this._mouseCapture( event ) ) { + return true; } - this.originalLabel = this.isInput ? this.element.val() : this.element.html(); - if ( this.originalLabel ) { - options.label = this.originalLabel; + this.mouseDelayMet = !this.options.delay; + if ( !this.mouseDelayMet ) { + this._mouseDelayTimer = setTimeout( function() { + that.mouseDelayMet = true; + }, this.options.delay ); } - return options; - }, - - _create: function() { - if ( !this.option.showLabel & !this.options.icon ) { - this.options.showLabel = true; + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = ( this._mouseStart( event ) !== false ); + if ( !this._mouseStarted ) { + event.preventDefault(); + return true; + } } - // We have to check the option again here even though we did in _getCreateOptions, - // because null may have been passed on init which would override what was set in - // _getCreateOptions - if ( this.options.disabled == null ) { - this.options.disabled = this.element[ 0 ].disabled || false; + // Click event may never have fired (Gecko & Opera) + if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, this.widgetName + ".preventClickEvent" ); } - this.hasTitle = !!this.element.attr( "title" ); + // These delegates are required to keep context + this._mouseMoveDelegate = function( event ) { + return that._mouseMove( event ); + }; + this._mouseUpDelegate = function( event ) { + return that._mouseUp( event ); + }; - // Check to see if the label needs to be set or if its already correct - if ( this.options.label && this.options.label !== this.originalLabel ) { - if ( this.isInput ) { - this.element.val( this.options.label ); - } else { - this.element.html( this.options.label ); - } - } - this._addClass( "ui-button", "ui-widget" ); - this._setOption( "disabled", this.options.disabled ); - this._enhance(); + this.document + .on( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .on( "mouseup." + this.widgetName, this._mouseUpDelegate ); - if ( this.element.is( "a" ) ) { - this._on( { - "keyup": function( event ) { - if ( event.keyCode === $.ui.keyCode.SPACE ) { - event.preventDefault(); + event.preventDefault(); - // Support: PhantomJS <= 1.9, IE 8 Only - // If a native click is available use it so we actually cause navigation - // otherwise just trigger a click event - if ( this.element[ 0 ].click ) { - this.element[ 0 ].click(); - } else { - this.element.trigger( "click" ); - } - } - } - } ); - } + mouseHandled = true; + return true; }, - _enhance: function() { - if ( !this.element.is( "button" ) ) { - this.element.attr( "role", "button" ); + _mouseMove: function( event ) { + + // Only check for mouseups outside the document if you've moved inside the document + // at least once. This prevents the firing of mouseup in the case of IE<9, which will + // fire a mousemove event if content is placed under the cursor. See #7778 + // Support: IE <9 + if ( this._mouseMoved ) { + + // IE mouseup check - mouseup happened when mouse was out of window + if ( $.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && + !event.button ) { + return this._mouseUp( event ); + + // Iframe mouseup check - mouseup occurred in another document + } else if ( !event.which ) { + + // Support: Safari <=8 - 9 + // Safari sets which to 0 if you press any of the following keys + // during a drag (#14461) + if ( event.originalEvent.altKey || event.originalEvent.ctrlKey || + event.originalEvent.metaKey || event.originalEvent.shiftKey ) { + this.ignoreMissingWhich = true; + } else if ( !this.ignoreMissingWhich ) { + return this._mouseUp( event ); + } + } } - if ( this.options.icon ) { - this._updateIcon( "icon", this.options.icon ); - this._updateTooltip(); + if ( event.which || event.button ) { + this._mouseMoved = true; } - }, - _updateTooltip: function() { - this.title = this.element.attr( "title" ); + if ( this._mouseStarted ) { + this._mouseDrag( event ); + return event.preventDefault(); + } - if ( !this.options.showLabel && !this.title ) { - this.element.attr( "title", this.options.label ); + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = + ( this._mouseStart( this._mouseDownEvent, event ) !== false ); + if ( this._mouseStarted ) { + this._mouseDrag( event ); + } else { + this._mouseUp( event ); + } } - }, - _updateIcon: function( option, value ) { - var icon = option !== "iconPosition", - position = icon ? this.options.iconPosition : value, - displayBlock = position === "top" || position === "bottom"; + return !this._mouseStarted; + }, - // Create icon - if ( !this.icon ) { - this.icon = $( "<span>" ); + _mouseUp: function( event ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); - this._addClass( this.icon, "ui-button-icon", "ui-icon" ); + if ( this._mouseStarted ) { + this._mouseStarted = false; - if ( !this.options.showLabel ) { - this._addClass( "ui-button-icon-only" ); + if ( event.target === this._mouseDownEvent.target ) { + $.data( event.target, this.widgetName + ".preventClickEvent", true ); } - } else if ( icon ) { - // If we are updating the icon remove the old icon class - this._removeClass( this.icon, null, this.options.icon ); + this._mouseStop( event ); } - // If we are updating the icon add the new icon class - if ( icon ) { - this._addClass( this.icon, null, value ); + if ( this._mouseDelayTimer ) { + clearTimeout( this._mouseDelayTimer ); + delete this._mouseDelayTimer; } - this._attachIcon( position ); + this.ignoreMissingWhich = false; + mouseHandled = false; + event.preventDefault(); + }, - // If the icon is on top or bottom we need to add the ui-widget-icon-block class and remove - // the iconSpace if there is one. - if ( displayBlock ) { - this._addClass( this.icon, null, "ui-widget-icon-block" ); - if ( this.iconSpace ) { - this.iconSpace.remove(); - } - } else { - - // Position is beginning or end so remove the ui-widget-icon-block class and add the - // space if it does not exist - if ( !this.iconSpace ) { - this.iconSpace = $( "<span> </span>" ); - this._addClass( this.iconSpace, "ui-button-icon-space" ); - } - this._removeClass( this.icon, null, "ui-wiget-icon-block" ); - this._attachIconSpace( position ); - } + _mouseDistanceMet: function( event ) { + return ( Math.max( + Math.abs( this._mouseDownEvent.pageX - event.pageX ), + Math.abs( this._mouseDownEvent.pageY - event.pageY ) + ) >= this.options.distance + ); }, - _destroy: function() { - this.element.removeAttr( "role" ); - - if ( this.icon ) { - this.icon.remove(); - } - if ( this.iconSpace ) { - this.iconSpace.remove(); - } - if ( !this.hasTitle ) { - this.element.removeAttr( "title" ); - } + _mouseDelayMet: function( /* event */ ) { + return this.mouseDelayMet; }, - _attachIconSpace: function( iconPosition ) { - this.icon[ /^(?:end|bottom)/.test( iconPosition ) ? "before" : "after" ]( this.iconSpace ); - }, + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function( /* event */ ) {}, + _mouseDrag: function( /* event */ ) {}, + _mouseStop: function( /* event */ ) {}, + _mouseCapture: function( /* event */ ) { + return true; + } +} ); - _attachIcon: function( iconPosition ) { - this.element[ /^(?:end|bottom)/.test( iconPosition ) ? "append" : "prepend" ]( this.icon ); - }, - _setOptions: function( options ) { - var newShowLabel = options.showLabel === undefined ? - this.options.showLabel : - options.showLabel, - newIcon = options.icon === undefined ? this.options.icon : options.icon; - if ( !newShowLabel && !newIcon ) { - options.showLabel = true; +// $.ui.plugin is deprecated. Use $.widget() extensions instead. +var plugin = $.ui.plugin = { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); } - this._super( options ); }, + call: function( instance, name, args, allowDisconnected ) { + var i, + set = instance.plugins[ name ]; - _setOption: function( key, value ) { - if ( key === "icon" ) { - if ( value ) { - this._updateIcon( key, value ); - } else if ( this.icon ) { - this.icon.remove(); - if ( this.iconSpace ) { - this.iconSpace.remove(); - } - } + if ( !set ) { + return; } - if ( key === "iconPosition" ) { - this._updateIcon( key, value ); + if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || + instance.element[ 0 ].parentNode.nodeType === 11 ) ) { + return; } - // Make sure we can't end up with a button that has neither text nor icon - if ( key === "showLabel" ) { - this._toggleClass( "ui-button-icon-only", null, !value ); - this._updateTooltip(); + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } } + } +}; - if ( key === "label" ) { - if ( this.isInput ) { - this.element.val( value ); - } else { - // If there is an icon, append it, else nothing then append the value - // this avoids removal of the icon when setting label text - this.element.html( value ); - if ( this.icon ) { - this._attachIcon( this.options.iconPosition ); - this._attachIconSpace( this.options.iconPosition ); - } - } + +var safeBlur = $.ui.safeBlur = function( element ) { + + // Support: IE9 - 10 only + // If the <body> is blurred, IE will switch windows, see #9420 + if ( element && element.nodeName.toLowerCase() !== "body" ) { + $( element ).trigger( "blur" ); + } +}; + + +/*! + * jQuery UI Draggable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Draggable +//>>group: Interactions +//>>description: Enables dragging functionality for any element. +//>>docs: http://api.jqueryui.com/draggable/ +//>>demos: http://jqueryui.com/draggable/ +//>>css.structure: ../../themes/base/draggable.css + + +$.widget( "ui.draggable", $.ui.mouse, { + version: "1.13.2", + widgetEventPrefix: "drag", + options: { + addClasses: true, + appendTo: "parent", + axis: false, + connectToSortable: false, + containment: false, + cursor: "auto", + cursorAt: false, + grid: false, + handle: false, + helper: "original", + iframeFix: false, + opacity: false, + refreshPositions: false, + revert: false, + revertDuration: 500, + scope: "default", + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + snap: false, + snapMode: "both", + snapTolerance: 20, + stack: false, + zIndex: false, + + // Callbacks + drag: null, + start: null, + stop: null + }, + _create: function() { + + if ( this.options.helper === "original" ) { + this._setPositionRelative(); + } + if ( this.options.addClasses ) { + this._addClass( "ui-draggable" ); } + this._setHandleClassName(); + + this._mouseInit(); + }, + _setOption: function( key, value ) { this._super( key, value ); + if ( key === "handle" ) { + this._removeHandleClassName(); + this._setHandleClassName(); + } + }, - if ( key === "disabled" ) { - this._toggleClass( null, "ui-state-disabled", value ); - this.element[ 0 ].disabled = value; - if ( value ) { - this.element.trigger( "blur" ); - } + _destroy: function() { + if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { + this.destroyOnClear = true; + return; } + this._removeHandleClassName(); + this._mouseDestroy(); }, - refresh: function() { + _mouseCapture: function( event ) { + var o = this.options; - // Make sure to only check disabled if its an element that supports this otherwise - // check for the disabled class to determine state - var isDisabled = this.element.is( "input, button" ) ? - this.element[ 0 ].disabled : this.element.hasClass( "ui-button-disabled" ); + // Among others, prevent a drag on a resizable-handle + if ( this.helper || o.disabled || + $( event.target ).closest( ".ui-resizable-handle" ).length > 0 ) { + return false; + } - if ( isDisabled !== this.options.disabled ) { - this._setOptions( { disabled: isDisabled } ); + //Quit if we're not on a valid handle + this.handle = this._getHandle( event ); + if ( !this.handle ) { + return false; } - this._updateTooltip(); - } -} ); + this._blurActiveElement( event ); -// DEPRECATED -if ( $.uiBackCompat !== false ) { + this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix ); - // Text and Icons options - $.widget( "ui.button", $.ui.button, { - options: { - text: true, - icons: { - primary: null, - secondary: null - } - }, + return true; - _create: function() { - if ( this.options.showLabel && !this.options.text ) { - this.options.showLabel = this.options.text; - } - if ( !this.options.showLabel && this.options.text ) { - this.options.text = this.options.showLabel; - } - if ( !this.options.icon && ( this.options.icons.primary || - this.options.icons.secondary ) ) { - if ( this.options.icons.primary ) { - this.options.icon = this.options.icons.primary; - } else { - this.options.icon = this.options.icons.secondary; - this.options.iconPosition = "end"; - } - } else if ( this.options.icon ) { - this.options.icons.primary = this.options.icon; - } - this._super(); - }, + }, - _setOption: function( key, value ) { - if ( key === "text" ) { - this._super( "showLabel", value ); - return; - } - if ( key === "showLabel" ) { - this.options.text = value; - } - if ( key === "icon" ) { - this.options.icons.primary = value; - } - if ( key === "icons" ) { - if ( value.primary ) { - this._super( "icon", value.primary ); - this._super( "iconPosition", "beginning" ); - } else if ( value.secondary ) { - this._super( "icon", value.secondary ); - this._super( "iconPosition", "end" ); - } - } - this._superApply( arguments ); + _blockFrames: function( selector ) { + this.iframeBlocks = this.document.find( selector ).map( function() { + var iframe = $( this ); + + return $( "<div>" ) + .css( "position", "absolute" ) + .appendTo( iframe.parent() ) + .outerWidth( iframe.outerWidth() ) + .outerHeight( iframe.outerHeight() ) + .offset( iframe.offset() )[ 0 ]; + } ); + }, + + _unblockFrames: function() { + if ( this.iframeBlocks ) { + this.iframeBlocks.remove(); + delete this.iframeBlocks; } - } ); + }, - $.fn.button = ( function( orig ) { - return function( options ) { - var isMethodCall = typeof options === "string"; - var args = Array.prototype.slice.call( arguments, 1 ); - var returnValue = this; + _blurActiveElement: function( event ) { + var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ), + target = $( event.target ); - if ( isMethodCall ) { + // Don't blur if the event occurred on an element that is within + // the currently focused element + // See #10527, #12472 + if ( target.closest( activeElement ).length ) { + return; + } - // If this is an empty collection, we need to have the instance method - // return undefined instead of the jQuery instance - if ( !this.length && options === "instance" ) { - returnValue = undefined; - } else { - this.each( function() { - var methodValue; - var type = $( this ).attr( "type" ); - var name = type !== "checkbox" && type !== "radio" ? - "button" : - "checkboxradio"; - var instance = $.data( this, "ui-" + name ); + // Blur any element that currently has focus, see #4261 + $.ui.safeBlur( activeElement ); + }, - if ( options === "instance" ) { - returnValue = instance; - return false; - } + _mouseStart: function( event ) { - if ( !instance ) { - return $.error( "cannot call methods on button" + - " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } + var o = this.options; - if ( typeof instance[ options ] !== "function" || - options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for button" + - " widget instance" ); - } + //Create and append the visible helper + this.helper = this._createHelper( event ); - methodValue = instance[ options ].apply( instance, args ); + this._addClass( this.helper, "ui-draggable-dragging" ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - } ); - } - } else { + //Cache the helper size + this._cacheHelperProportions(); - // Allow multiple hashes to be passed on init - if ( args.length ) { - options = $.widget.extend.apply( null, [ options ].concat( args ) ); - } + //If ddmanager is used for droppables, set the global draggable + if ( $.ui.ddmanager ) { + $.ui.ddmanager.current = this; + } - this.each( function() { - var type = $( this ).attr( "type" ); - var name = type !== "checkbox" && type !== "radio" ? "button" : "checkboxradio"; - var instance = $.data( this, "ui-" + name ); + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ - if ( instance ) { - instance.option( options || {} ); - if ( instance._init ) { - instance._init(); - } - } else { - if ( name === "button" ) { - orig.call( $( this ), options ); - return; - } + //Cache the margins of the original element + this._cacheMargins(); - $( this ).checkboxradio( $.extend( { icon: false }, options ) ); - } - } ); - } + //Store the helper's css position + this.cssPosition = this.helper.css( "position" ); + this.scrollParent = this.helper.scrollParent( true ); + this.offsetParent = this.helper.offsetParent(); + this.hasFixedAncestor = this.helper.parents().filter( function() { + return $( this ).css( "position" ) === "fixed"; + } ).length > 0; - return returnValue; - }; - } )( $.fn.button ); + //The element's absolute position on the page minus margins + this.positionAbs = this.element.offset(); + this._refreshOffsets( event ); - $.fn.buttonset = function() { - if ( !$.ui.controlgroup ) { - $.error( "Controlgroup widget missing" ); - } - if ( arguments[ 0 ] === "option" && arguments[ 1 ] === "items" && arguments[ 2 ] ) { - return this.controlgroup.apply( this, - [ arguments[ 0 ], "items.button", arguments[ 2 ] ] ); - } - if ( arguments[ 0 ] === "option" && arguments[ 1 ] === "items" ) { - return this.controlgroup.apply( this, [ arguments[ 0 ], "items.button" ] ); - } - if ( typeof arguments[ 0 ] === "object" && arguments[ 0 ].items ) { - arguments[ 0 ].items = { - button: arguments[ 0 ].items - }; + //Generate the original position + this.originalPosition = this.position = this._generatePosition( event, false ); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + if ( o.cursorAt ) { + this._adjustOffsetFromHelper( o.cursorAt ); } - return this.controlgroup.apply( this, arguments ); - }; -} -var widgetsButton = $.ui.button; + //Set a containment if given in the options + this._setContainment(); + //Trigger event + callbacks + if ( this._trigger( "start", event ) === false ) { + this._clear(); + return false; + } -/* eslint-disable max-len, camelcase */ -/*! - * jQuery UI Datepicker 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + //Recache the helper size + this._cacheHelperProportions(); -//>>label: Datepicker -//>>group: Widgets -//>>description: Displays a calendar from an input or inline for selecting dates. -//>>docs: http://api.jqueryui.com/datepicker/ -//>>demos: http://jqueryui.com/datepicker/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/datepicker.css -//>>css.theme: ../../themes/base/theme.css + //Prepare the droppable offsets + if ( $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( this, event ); + } + // Execute the drag once - this causes the helper not to be visible before getting its + // correct position + this._mouseDrag( event, true ); -$.extend( $.ui, { datepicker: { version: "1.13.1" } } ); + // If the ddmanager is used for droppables, inform the manager that dragging has started + // (see #5003) + if ( $.ui.ddmanager ) { + $.ui.ddmanager.dragStart( this, event ); + } -var datepicker_instActive; + return true; + }, -function datepicker_getZindex( elem ) { - var position, value; - while ( elem.length && elem[ 0 ] !== document ) { + _refreshOffsets: function( event ) { + this.offset = { + top: this.positionAbs.top - this.margins.top, + left: this.positionAbs.left - this.margins.left, + scroll: false, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() + }; - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { + this.offset.click = { + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }; + }, - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> - value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } + _mouseDrag: function( event, noPropagation ) { + + // reset any necessary cached properties (see #5009) + if ( this.hasFixedAncestor ) { + this.offset.parent = this._getParentOffset(); } - elem = elem.parent(); - } - return 0; -} + //Compute the helpers position + this.position = this._generatePosition( event, true ); + this.positionAbs = this._convertPositionTo( "absolute" ); -/* Date picker manager. - Use the singleton instance of this class, $.datepicker, to interact with the date picker. - Settings for (groups of) date pickers are maintained in an instance object, - allowing multiple different settings on the same page. */ - -function Datepicker() { - this._curInst = null; // The current instance in use - this._keyEvent = false; // If the last event was a key event - this._disabledInputs = []; // List of date picker inputs that have been disabled - this._datepickerShowing = false; // True if the popup picker is showing , false if not - this._inDialog = false; // True if showing within a "dialog", false if not - this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division - this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class - this._appendClass = "ui-datepicker-append"; // The name of the append marker class - this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class - this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class - this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class - this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class - this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class - this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class - this.regional = []; // Available regional settings, indexed by language code - this.regional[ "" ] = { // Default regional settings - closeText: "Done", // Display text for close link - prevText: "Prev", // Display text for previous month link - nextText: "Next", // Display text for next month link - currentText: "Today", // Display text for current month link - monthNames: [ "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" ], // Names of months for drop-down and formatting - monthNamesShort: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], // For formatting - dayNames: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], // For formatting - dayNamesShort: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], // For formatting - dayNamesMin: [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ], // Column headings for days starting at Sunday - weekHeader: "Wk", // Column header for week of the year - dateFormat: "mm/dd/yy", // See format options on parseDate - firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... - isRTL: false, // True if right-to-left language, false if left-to-right - showMonthAfterYear: false, // True if the year select precedes month, false for month then year - yearSuffix: "", // Additional text to append to the year in the month headers, - selectMonthLabel: "Select month", // Invisible label for month selector - selectYearLabel: "Select year" // Invisible label for year selector - }; - this._defaults = { // Global defaults for all the date picker instances - showOn: "focus", // "focus" for popup on focus, - // "button" for trigger button, or "both" for either - showAnim: "fadeIn", // Name of jQuery animation for popup - showOptions: {}, // Options for enhanced animations - defaultDate: null, // Used when field is blank: actual date, - // +/-number for offset from today, null for today - appendText: "", // Display text following the input box, e.g. showing the format - buttonText: "...", // Text for trigger button - buttonImage: "", // URL for trigger button image - buttonImageOnly: false, // True if the image appears alone, false if it appears on a button - hideIfNoPrevNext: false, // True to hide next/previous month links - // if not applicable, false to just disable them - navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links - gotoCurrent: false, // True if today link goes back to current selection instead - changeMonth: false, // True if month can be selected directly, false if only prev/next - changeYear: false, // True if year can be selected directly, false if only prev/next - yearRange: "c-10:c+10", // Range of years to display in drop-down, - // either relative to today's year (-nn:+nn), relative to currently displayed year - // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) - showOtherMonths: false, // True to show dates in other months, false to leave blank - selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable - showWeek: false, // True to show week of the year, false to not show it - calculateWeek: this.iso8601Week, // How to calculate the week of the year, - // takes a Date and returns the number of the week for it - shortYearCutoff: "+10", // Short year values < this are in the current century, - // > this are in the previous century, - // string value starting with "+" for current year + value - minDate: null, // The earliest selectable date, or null for no limit - maxDate: null, // The latest selectable date, or null for no limit - duration: "fast", // Duration of display/closure - beforeShowDay: null, // Function that takes a date and returns an array with - // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", - // [2] = cell title (optional), e.g. $.datepicker.noWeekends - beforeShow: null, // Function that takes an input field and - // returns a set of custom settings for the date picker - onSelect: null, // Define a callback function when a date is selected - onChangeMonthYear: null, // Define a callback function when the month or year is changed - onClose: null, // Define a callback function when the datepicker is closed - onUpdateDatepicker: null, // Define a callback function when the datepicker is updated - numberOfMonths: 1, // Number of months to show at a time - showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) - stepMonths: 1, // Number of months to step back/forward - stepBigMonths: 12, // Number of months to step back/forward for the big links - altField: "", // Selector for an alternate field to store selected dates into - altFormat: "", // The date format to use for the alternate field - constrainInput: true, // The input is constrained by the current date format - showButtonPanel: false, // True to show button panel, false to not show it - autoSize: false, // True to size the input for the date format, false to leave as is - disabled: false // The initial disabled state - }; - $.extend( this._defaults, this.regional[ "" ] ); - this.regional.en = $.extend( true, {}, this.regional[ "" ] ); - this.regional[ "en-US" ] = $.extend( true, {}, this.regional.en ); - this.dpDiv = datepicker_bindHover( $( "<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>" ) ); -} - -$.extend( Datepicker.prototype, { + //Call plugins and callbacks and use the resulting position if something is returned + if ( !noPropagation ) { + var ui = this._uiHash(); + if ( this._trigger( "drag", event, ui ) === false ) { + this._mouseUp( new $.Event( "mouseup", event ) ); + return false; + } + this.position = ui.position; + } - /* Class name added to elements to indicate already configured with a date picker. */ - markerClassName: "hasDatepicker", + this.helper[ 0 ].style.left = this.position.left + "px"; + this.helper[ 0 ].style.top = this.position.top + "px"; - //Keep track of the maximum number of rows displayed (see #7043) - maxRows: 4, + if ( $.ui.ddmanager ) { + $.ui.ddmanager.drag( this, event ); + } - // TODO rename to "widget" when switching to widget factory - _widgetDatepicker: function() { - return this.dpDiv; + return false; }, - /* Override the default settings for all instances of the date picker. - * @param settings object - the new settings to use as defaults (anonymous object) - * @return the manager object - */ - setDefaults: function( settings ) { - datepicker_extendRemove( this._defaults, settings || {} ); - return this; - }, + _mouseStop: function( event ) { - /* Attach the date picker to a jQuery selection. - * @param target element - the target input field or division or span - * @param settings object - the new settings to use for this date picker instance (anonymous) - */ - _attachDatepicker: function( target, settings ) { - var nodeName, inline, inst; - nodeName = target.nodeName.toLowerCase(); - inline = ( nodeName === "div" || nodeName === "span" ); - if ( !target.id ) { - this.uuid += 1; - target.id = "dp" + this.uuid; + //If we are using droppables, inform the manager about the drop + var that = this, + dropped = false; + if ( $.ui.ddmanager && !this.options.dropBehaviour ) { + dropped = $.ui.ddmanager.drop( this, event ); } - inst = this._newInst( $( target ), inline ); - inst.settings = $.extend( {}, settings || {} ); - if ( nodeName === "input" ) { - this._connectDatepicker( target, inst ); - } else if ( inline ) { - this._inlineDatepicker( target, inst ); + + //if a drop comes from outside (a sortable) + if ( this.dropped ) { + dropped = this.dropped; + this.dropped = false; } - }, - /* Create a new instance object. */ - _newInst: function( target, inline ) { - var id = target[ 0 ].id.replace( /([^A-Za-z0-9_\-])/g, "\\\\$1" ); // escape jQuery meta chars - return { id: id, input: target, // associated target - selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection - drawMonth: 0, drawYear: 0, // month being drawn - inline: inline, // is datepicker inline or not - dpDiv: ( !inline ? this.dpDiv : // presentation div - datepicker_bindHover( $( "<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>" ) ) ) }; + if ( ( this.options.revert === "invalid" && !dropped ) || + ( this.options.revert === "valid" && dropped ) || + this.options.revert === true || ( typeof this.options.revert === "function" && + this.options.revert.call( this.element, dropped ) ) + ) { + $( this.helper ).animate( + this.originalPosition, + parseInt( this.options.revertDuration, 10 ), + function() { + if ( that._trigger( "stop", event ) !== false ) { + that._clear(); + } + } + ); + } else { + if ( this._trigger( "stop", event ) !== false ) { + this._clear(); + } + } + + return false; }, - /* Attach the date picker to an input field. */ - _connectDatepicker: function( target, inst ) { - var input = $( target ); - inst.append = $( [] ); - inst.trigger = $( [] ); - if ( input.hasClass( this.markerClassName ) ) { - return; + _mouseUp: function( event ) { + this._unblockFrames(); + + // If the ddmanager is used for droppables, inform the manager that dragging has stopped + // (see #5003) + if ( $.ui.ddmanager ) { + $.ui.ddmanager.dragStop( this, event ); } - this._attachments( input, inst ); - input.addClass( this.markerClassName ).on( "keydown", this._doKeyDown ). - on( "keypress", this._doKeyPress ).on( "keyup", this._doKeyUp ); - this._autoSize( inst ); - $.data( target, "datepicker", inst ); - //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) - if ( inst.settings.disabled ) { - this._disableDatepicker( target ); + // Only need to focus if the event occurred on the draggable itself, see #10527 + if ( this.handleElement.is( event.target ) ) { + + // The interaction is over; whether or not the click resulted in a drag, + // focus the element + this.element.trigger( "focus" ); } + + return $.ui.mouse.prototype._mouseUp.call( this, event ); }, - /* Make attachments based on settings. */ - _attachments: function( input, inst ) { - var showOn, buttonText, buttonImage, - appendText = this._get( inst, "appendText" ), - isRTL = this._get( inst, "isRTL" ); + cancel: function() { - if ( inst.append ) { - inst.append.remove(); - } - if ( appendText ) { - inst.append = $( "<span>" ) - .addClass( this._appendClass ) - .text( appendText ); - input[ isRTL ? "before" : "after" ]( inst.append ); + if ( this.helper.is( ".ui-draggable-dragging" ) ) { + this._mouseUp( new $.Event( "mouseup", { target: this.element[ 0 ] } ) ); + } else { + this._clear(); } - input.off( "focus", this._showDatepicker ); + return this; - if ( inst.trigger ) { - inst.trigger.remove(); - } + }, - showOn = this._get( inst, "showOn" ); - if ( showOn === "focus" || showOn === "both" ) { // pop-up date picker when in the marked field - input.on( "focus", this._showDatepicker ); - } - if ( showOn === "button" || showOn === "both" ) { // pop-up date picker when button clicked - buttonText = this._get( inst, "buttonText" ); - buttonImage = this._get( inst, "buttonImage" ); + _getHandle: function( event ) { + return this.options.handle ? + !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : + true; + }, - if ( this._get( inst, "buttonImageOnly" ) ) { - inst.trigger = $( "<img>" ) - .addClass( this._triggerClass ) - .attr( { - src: buttonImage, - alt: buttonText, - title: buttonText - } ); - } else { - inst.trigger = $( "<button type='button'>" ) - .addClass( this._triggerClass ); - if ( buttonImage ) { - inst.trigger.html( - $( "<img>" ) - .attr( { - src: buttonImage, - alt: buttonText, - title: buttonText - } ) - ); - } else { - inst.trigger.text( buttonText ); - } - } + _setHandleClassName: function() { + this.handleElement = this.options.handle ? + this.element.find( this.options.handle ) : this.element; + this._addClass( this.handleElement, "ui-draggable-handle" ); + }, - input[ isRTL ? "before" : "after" ]( inst.trigger ); - inst.trigger.on( "click", function() { - if ( $.datepicker._datepickerShowing && $.datepicker._lastInput === input[ 0 ] ) { - $.datepicker._hideDatepicker(); - } else if ( $.datepicker._datepickerShowing && $.datepicker._lastInput !== input[ 0 ] ) { - $.datepicker._hideDatepicker(); - $.datepicker._showDatepicker( input[ 0 ] ); - } else { - $.datepicker._showDatepicker( input[ 0 ] ); - } - return false; - } ); - } + _removeHandleClassName: function() { + this._removeClass( this.handleElement, "ui-draggable-handle" ); }, - /* Apply the maximum length for the date format. */ - _autoSize: function( inst ) { - if ( this._get( inst, "autoSize" ) && !inst.inline ) { - var findMax, max, maxI, i, - date = new Date( 2009, 12 - 1, 20 ), // Ensure double digits - dateFormat = this._get( inst, "dateFormat" ); + _createHelper: function( event ) { - if ( dateFormat.match( /[DM]/ ) ) { - findMax = function( names ) { - max = 0; - maxI = 0; - for ( i = 0; i < names.length; i++ ) { - if ( names[ i ].length > max ) { - max = names[ i ].length; - maxI = i; - } - } - return maxI; - }; - date.setMonth( findMax( this._get( inst, ( dateFormat.match( /MM/ ) ? - "monthNames" : "monthNamesShort" ) ) ) ); - date.setDate( findMax( this._get( inst, ( dateFormat.match( /DD/ ) ? - "dayNames" : "dayNamesShort" ) ) ) + 20 - date.getDay() ); - } - inst.input.attr( "size", this._formatDate( inst, date ).length ); + var o = this.options, + helperIsFunction = typeof o.helper === "function", + helper = helperIsFunction ? + $( o.helper.apply( this.element[ 0 ], [ event ] ) ) : + ( o.helper === "clone" ? + this.element.clone().removeAttr( "id" ) : + this.element ); + + if ( !helper.parents( "body" ).length ) { + helper.appendTo( ( o.appendTo === "parent" ? + this.element[ 0 ].parentNode : + o.appendTo ) ); } - }, - /* Attach an inline date picker to a div. */ - _inlineDatepicker: function( target, inst ) { - var divSpan = $( target ); - if ( divSpan.hasClass( this.markerClassName ) ) { - return; + // Http://bugs.jqueryui.com/ticket/9446 + // a helper function can return the original element + // which wouldn't have been set to relative in _create + if ( helperIsFunction && helper[ 0 ] === this.element[ 0 ] ) { + this._setPositionRelative(); } - divSpan.addClass( this.markerClassName ).append( inst.dpDiv ); - $.data( target, "datepicker", inst ); - this._setDate( inst, this._getDefaultDate( inst ), true ); - this._updateDatepicker( inst ); - this._updateAlternate( inst ); - //If disabled option is true, disable the datepicker before showing it (see ticket #5665) - if ( inst.settings.disabled ) { - this._disableDatepicker( target ); + if ( helper[ 0 ] !== this.element[ 0 ] && + !( /(fixed|absolute)/ ).test( helper.css( "position" ) ) ) { + helper.css( "position", "absolute" ); } - // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements - // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height - inst.dpDiv.css( "display", "block" ); - }, + return helper; - /* Pop-up the date picker in a "dialog" box. - * @param input element - ignored - * @param date string or Date - the initial date to display - * @param onSelect function - the function to call when a date is selected - * @param settings object - update the dialog date picker instance's settings (anonymous object) - * @param pos int[2] - coordinates for the dialog's position within the screen or - * event - with x/y coordinates or - * leave empty for default (screen centre) - * @return the manager object - */ - _dialogDatepicker: function( input, date, onSelect, settings, pos ) { - var id, browserWidth, browserHeight, scrollX, scrollY, - inst = this._dialogInst; // internal instance + }, - if ( !inst ) { - this.uuid += 1; - id = "dp" + this.uuid; - this._dialogInput = $( "<input type='text' id='" + id + - "' style='position: absolute; top: -100px; width: 0px;'/>" ); - this._dialogInput.on( "keydown", this._doKeyDown ); - $( "body" ).append( this._dialogInput ); - inst = this._dialogInst = this._newInst( this._dialogInput, false ); - inst.settings = {}; - $.data( this._dialogInput[ 0 ], "datepicker", inst ); + _setPositionRelative: function() { + if ( !( /^(?:r|a|f)/ ).test( this.element.css( "position" ) ) ) { + this.element[ 0 ].style.position = "relative"; } - datepicker_extendRemove( inst.settings, settings || {} ); - date = ( date && date.constructor === Date ? this._formatDate( inst, date ) : date ); - this._dialogInput.val( date ); + }, - this._pos = ( pos ? ( pos.length ? pos : [ pos.pageX, pos.pageY ] ) : null ); - if ( !this._pos ) { - browserWidth = document.documentElement.clientWidth; - browserHeight = document.documentElement.clientHeight; - scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; - scrollY = document.documentElement.scrollTop || document.body.scrollTop; - this._pos = // should use actual width/height below - [ ( browserWidth / 2 ) - 100 + scrollX, ( browserHeight / 2 ) - 150 + scrollY ]; + _adjustOffsetFromHelper: function( obj ) { + if ( typeof obj === "string" ) { + obj = obj.split( " " ); } - - // Move input on screen for focus, but hidden behind dialog - this._dialogInput.css( "left", ( this._pos[ 0 ] + 20 ) + "px" ).css( "top", this._pos[ 1 ] + "px" ); - inst.settings.onSelect = onSelect; - this._inDialog = true; - this.dpDiv.addClass( this._dialogClass ); - this._showDatepicker( this._dialogInput[ 0 ] ); - if ( $.blockUI ) { - $.blockUI( this.dpDiv ); + if ( Array.isArray( obj ) ) { + obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; + } + if ( "left" in obj ) { + this.offset.click.left = obj.left + this.margins.left; + } + if ( "right" in obj ) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ( "top" in obj ) { + this.offset.click.top = obj.top + this.margins.top; + } + if ( "bottom" in obj ) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; } - $.data( this._dialogInput[ 0 ], "datepicker", inst ); - return this; }, - /* Detach a datepicker from its control. - * @param target element - the target input field or division or span - */ - _destroyDatepicker: function( target ) { - var nodeName, - $target = $( target ), - inst = $.data( target, "datepicker" ); + _isRootNode: function( element ) { + return ( /(html|body)/i ).test( element.tagName ) || element === this.document[ 0 ]; + }, - if ( !$target.hasClass( this.markerClassName ) ) { - return; - } + _getParentOffset: function() { - nodeName = target.nodeName.toLowerCase(); - $.removeData( target, "datepicker" ); - if ( nodeName === "input" ) { - inst.append.remove(); - inst.trigger.remove(); - $target.removeClass( this.markerClassName ). - off( "focus", this._showDatepicker ). - off( "keydown", this._doKeyDown ). - off( "keypress", this._doKeyPress ). - off( "keyup", this._doKeyUp ); - } else if ( nodeName === "div" || nodeName === "span" ) { - $target.removeClass( this.markerClassName ).empty(); + //Get the offsetParent and cache its position + var po = this.offsetParent.offset(), + document = this.document[ 0 ]; + + // This is a special case where we need to modify a offset calculated on start, since the + // following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the + // next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't + // the document, which means that the scroll is included in the initial calculation of the + // offset of the parent, and never recalculated upon drag + if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== document && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); } - if ( datepicker_instActive === inst ) { - datepicker_instActive = null; - this._curInst = null; + if ( this._isRootNode( this.offsetParent[ 0 ] ) ) { + po = { top: 0, left: 0 }; } - }, - /* Enable the date picker to a jQuery selection. - * @param target element - the target input field or division or span - */ - _enableDatepicker: function( target ) { - var nodeName, inline, - $target = $( target ), - inst = $.data( target, "datepicker" ); + return { + top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), + left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) + }; - if ( !$target.hasClass( this.markerClassName ) ) { - return; - } + }, - nodeName = target.nodeName.toLowerCase(); - if ( nodeName === "input" ) { - target.disabled = false; - inst.trigger.filter( "button" ). - each( function() { - this.disabled = false; - } ).end(). - filter( "img" ).css( { opacity: "1.0", cursor: "" } ); - } else if ( nodeName === "div" || nodeName === "span" ) { - inline = $target.children( "." + this._inlineClass ); - inline.children().removeClass( "ui-state-disabled" ); - inline.find( "select.ui-datepicker-month, select.ui-datepicker-year" ). - prop( "disabled", false ); + _getRelativeOffset: function() { + if ( this.cssPosition !== "relative" ) { + return { top: 0, left: 0 }; } - this._disabledInputs = $.map( this._disabledInputs, - // Delete entry - function( value ) { - return ( value === target ? null : value ); - } ); - }, + var p = this.element.position(), + scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); - /* Disable the date picker to a jQuery selection. - * @param target element - the target input field or division or span - */ - _disableDatepicker: function( target ) { - var nodeName, inline, - $target = $( target ), - inst = $.data( target, "datepicker" ); - - if ( !$target.hasClass( this.markerClassName ) ) { - return; - } - - nodeName = target.nodeName.toLowerCase(); - if ( nodeName === "input" ) { - target.disabled = true; - inst.trigger.filter( "button" ). - each( function() { - this.disabled = true; - } ).end(). - filter( "img" ).css( { opacity: "0.5", cursor: "default" } ); - } else if ( nodeName === "div" || nodeName === "span" ) { - inline = $target.children( "." + this._inlineClass ); - inline.children().addClass( "ui-state-disabled" ); - inline.find( "select.ui-datepicker-month, select.ui-datepicker-year" ). - prop( "disabled", true ); - } - this._disabledInputs = $.map( this._disabledInputs, + return { + top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + + ( !scrollIsRootNode ? this.scrollParent.scrollTop() : 0 ), + left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + + ( !scrollIsRootNode ? this.scrollParent.scrollLeft() : 0 ) + }; - // Delete entry - function( value ) { - return ( value === target ? null : value ); - } ); - this._disabledInputs[ this._disabledInputs.length ] = target; }, - /* Is the first field in a jQuery collection disabled as a datepicker? - * @param target element - the target input field or division or span - * @return boolean - true if disabled, false if enabled - */ - _isDisabledDatepicker: function( target ) { - if ( !target ) { - return false; - } - for ( var i = 0; i < this._disabledInputs.length; i++ ) { - if ( this._disabledInputs[ i ] === target ) { - return true; - } - } - return false; + _cacheMargins: function() { + this.margins = { + left: ( parseInt( this.element.css( "marginLeft" ), 10 ) || 0 ), + top: ( parseInt( this.element.css( "marginTop" ), 10 ) || 0 ), + right: ( parseInt( this.element.css( "marginRight" ), 10 ) || 0 ), + bottom: ( parseInt( this.element.css( "marginBottom" ), 10 ) || 0 ) + }; }, - /* Retrieve the instance data for the target control. - * @param target element - the target input field or division or span - * @return object - the associated instance data - * @throws error if a jQuery problem getting data - */ - _getInst: function( target ) { - try { - return $.data( target, "datepicker" ); - } catch ( err ) { - throw "Missing instance data for this datepicker"; - } + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; }, - /* Update or retrieve the settings for a date picker attached to an input field or division. - * @param target element - the target input field or division or span - * @param name object - the new settings to update or - * string - the name of the setting to change or retrieve, - * when retrieving also "all" for all instance settings or - * "defaults" for all global defaults - * @param value any - the new value for the setting - * (omit if above is an object or to retrieve a value) - */ - _optionDatepicker: function( target, name, value ) { - var settings, date, minDate, maxDate, - inst = this._getInst( target ); + _setContainment: function() { - if ( arguments.length === 2 && typeof name === "string" ) { - return ( name === "defaults" ? $.extend( {}, $.datepicker._defaults ) : - ( inst ? ( name === "all" ? $.extend( {}, inst.settings ) : - this._get( inst, name ) ) : null ) ); + var isUserScrollable, c, ce, + o = this.options, + document = this.document[ 0 ]; + + this.relativeContainer = null; + + if ( !o.containment ) { + this.containment = null; + return; } - settings = name || {}; - if ( typeof name === "string" ) { - settings = {}; - settings[ name ] = value; + if ( o.containment === "window" ) { + this.containment = [ + $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, + $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, + $( window ).scrollLeft() + $( window ).width() - + this.helperProportions.width - this.margins.left, + $( window ).scrollTop() + + ( $( window ).height() || document.body.parentNode.scrollHeight ) - + this.helperProportions.height - this.margins.top + ]; + return; } - if ( inst ) { - if ( this._curInst === inst ) { - this._hideDatepicker(); - } + if ( o.containment === "document" ) { + this.containment = [ + 0, + 0, + $( document ).width() - this.helperProportions.width - this.margins.left, + ( $( document ).height() || document.body.parentNode.scrollHeight ) - + this.helperProportions.height - this.margins.top + ]; + return; + } - date = this._getDateDatepicker( target, true ); - minDate = this._getMinMaxDate( inst, "min" ); - maxDate = this._getMinMaxDate( inst, "max" ); - datepicker_extendRemove( inst.settings, settings ); + if ( o.containment.constructor === Array ) { + this.containment = o.containment; + return; + } - // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided - if ( minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined ) { - inst.settings.minDate = this._formatDate( inst, minDate ); - } - if ( maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined ) { - inst.settings.maxDate = this._formatDate( inst, maxDate ); - } - if ( "disabled" in settings ) { - if ( settings.disabled ) { - this._disableDatepicker( target ); - } else { - this._enableDatepicker( target ); - } - } - this._attachments( $( target ), inst ); - this._autoSize( inst ); - this._setDate( inst, date ); - this._updateAlternate( inst ); - this._updateDatepicker( inst ); + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; } - }, - // Change method deprecated - _changeDatepicker: function( target, name, value ) { - this._optionDatepicker( target, name, value ); - }, + c = $( o.containment ); + ce = c[ 0 ]; - /* Redraw the date picker attached to an input field or division. - * @param target element - the target input field or division or span - */ - _refreshDatepicker: function( target ) { - var inst = this._getInst( target ); - if ( inst ) { - this._updateDatepicker( inst ); + if ( !ce ) { + return; } - }, - /* Set the dates for a jQuery selection. - * @param target element - the target input field or division or span - * @param date Date - the new date - */ - _setDateDatepicker: function( target, date ) { - var inst = this._getInst( target ); - if ( inst ) { - this._setDate( inst, date ); - this._updateDatepicker( inst ); - this._updateAlternate( inst ); - } + isUserScrollable = /(scroll|auto)/.test( c.css( "overflow" ) ); + + this.containment = [ + ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), + ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ), + ( isUserScrollable ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - + ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - + ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - + this.helperProportions.width - + this.margins.left - + this.margins.right, + ( isUserScrollable ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - + ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - + ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - + this.helperProportions.height - + this.margins.top - + this.margins.bottom + ]; + this.relativeContainer = c; }, - /* Get the date(s) for the first entry in a jQuery selection. - * @param target element - the target input field or division or span - * @param noDefault boolean - true if no default date is to be used - * @return Date - the current date - */ - _getDateDatepicker: function( target, noDefault ) { - var inst = this._getInst( target ); - if ( inst && !inst.inline ) { - this._setDateFromField( inst, noDefault ); + _convertPositionTo: function( d, pos ) { + + if ( !pos ) { + pos = this.position; } - return ( inst ? this._getDate( inst ) : null ); - }, - /* Handle keystrokes. */ - _doKeyDown: function( event ) { - var onSelect, dateStr, sel, - inst = $.datepicker._getInst( event.target ), - handled = true, - isRTL = inst.dpDiv.is( ".ui-datepicker-rtl" ); + var mod = d === "absolute" ? 1 : -1, + scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); - inst._keyEvent = true; - if ( $.datepicker._datepickerShowing ) { - switch ( event.keyCode ) { - case 9: $.datepicker._hideDatepicker(); - handled = false; - break; // hide on tab out - case 13: sel = $( "td." + $.datepicker._dayOverClass + ":not(." + - $.datepicker._currentClass + ")", inst.dpDiv ); - if ( sel[ 0 ] ) { - $.datepicker._selectDay( event.target, inst.selectedMonth, inst.selectedYear, sel[ 0 ] ); - } + return { + top: ( - onSelect = $.datepicker._get( inst, "onSelect" ); - if ( onSelect ) { - dateStr = $.datepicker._formatDate( inst ); + // The absolute mouse position + pos.top + - // Trigger custom callback - onSelect.apply( ( inst.input ? inst.input[ 0 ] : null ), [ dateStr, inst ] ); - } else { - $.datepicker._hideDatepicker(); - } + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top * mod + - return false; // don't submit the form - case 27: $.datepicker._hideDatepicker(); - break; // hide on escape - case 33: $.datepicker._adjustDate( event.target, ( event.ctrlKey ? - -$.datepicker._get( inst, "stepBigMonths" ) : - -$.datepicker._get( inst, "stepMonths" ) ), "M" ); - break; // previous month/year on page up/+ ctrl - case 34: $.datepicker._adjustDate( event.target, ( event.ctrlKey ? - +$.datepicker._get( inst, "stepBigMonths" ) : - +$.datepicker._get( inst, "stepMonths" ) ), "M" ); - break; // next month/year on page down/+ ctrl - case 35: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._clearDate( event.target ); - } - handled = event.ctrlKey || event.metaKey; - break; // clear on ctrl or command +end - case 36: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._gotoToday( event.target ); - } - handled = event.ctrlKey || event.metaKey; - break; // current on ctrl or command +home - case 37: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._adjustDate( event.target, ( isRTL ? +1 : -1 ), "D" ); - } - handled = event.ctrlKey || event.metaKey; - - // -1 day on ctrl or command +left - if ( event.originalEvent.altKey ) { - $.datepicker._adjustDate( event.target, ( event.ctrlKey ? - -$.datepicker._get( inst, "stepBigMonths" ) : - -$.datepicker._get( inst, "stepMonths" ) ), "M" ); - } + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top * mod - + ( ( this.cssPosition === "fixed" ? + -this.offset.scroll.top : + ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) * mod ) + ), + left: ( - // next month/year on alt +left on Mac - break; - case 38: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._adjustDate( event.target, -7, "D" ); - } - handled = event.ctrlKey || event.metaKey; - break; // -1 week on ctrl or command +up - case 39: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._adjustDate( event.target, ( isRTL ? -1 : +1 ), "D" ); - } - handled = event.ctrlKey || event.metaKey; + // The absolute mouse position + pos.left + - // +1 day on ctrl or command +right - if ( event.originalEvent.altKey ) { - $.datepicker._adjustDate( event.target, ( event.ctrlKey ? - +$.datepicker._get( inst, "stepBigMonths" ) : - +$.datepicker._get( inst, "stepMonths" ) ), "M" ); - } + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left * mod + - // next month/year on alt +right - break; - case 40: if ( event.ctrlKey || event.metaKey ) { - $.datepicker._adjustDate( event.target, +7, "D" ); - } - handled = event.ctrlKey || event.metaKey; - break; // +1 week on ctrl or command +down - default: handled = false; - } - } else if ( event.keyCode === 36 && event.ctrlKey ) { // display the date picker on ctrl+home - $.datepicker._showDatepicker( this ); - } else { - handled = false; - } + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left * mod - + ( ( this.cssPosition === "fixed" ? + -this.offset.scroll.left : + ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) * mod ) + ) + }; - if ( handled ) { - event.preventDefault(); - event.stopPropagation(); - } }, - /* Filter entered characters - based on date format. */ - _doKeyPress: function( event ) { - var chars, chr, - inst = $.datepicker._getInst( event.target ); + _generatePosition: function( event, constrainPosition ) { - if ( $.datepicker._get( inst, "constrainInput" ) ) { - chars = $.datepicker._possibleChars( $.datepicker._get( inst, "dateFormat" ) ); - chr = String.fromCharCode( event.charCode == null ? event.keyCode : event.charCode ); - return event.ctrlKey || event.metaKey || ( chr < " " || !chars || chars.indexOf( chr ) > -1 ); + var containment, co, top, left, + o = this.options, + scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ), + pageX = event.pageX, + pageY = event.pageY; + + // Cache the scroll + if ( !scrollIsRootNode || !this.offset.scroll ) { + this.offset.scroll = { + top: this.scrollParent.scrollTop(), + left: this.scrollParent.scrollLeft() + }; } - }, - /* Synchronise manual entry and field/alternate field. */ - _doKeyUp: function( event ) { - var date, - inst = $.datepicker._getInst( event.target ); + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ - if ( inst.input.val() !== inst.lastVal ) { - try { - date = $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ), - ( inst.input ? inst.input.val() : null ), - $.datepicker._getFormatConfig( inst ) ); + // If we are not dragging yet, we won't check for options + if ( constrainPosition ) { + if ( this.containment ) { + if ( this.relativeContainer ) { + co = this.relativeContainer.offset(); + containment = [ + this.containment[ 0 ] + co.left, + this.containment[ 1 ] + co.top, + this.containment[ 2 ] + co.left, + this.containment[ 3 ] + co.top + ]; + } else { + containment = this.containment; + } - if ( date ) { // only if valid - $.datepicker._setDateFromField( inst ); - $.datepicker._updateAlternate( inst ); - $.datepicker._updateDatepicker( inst ); + if ( event.pageX - this.offset.click.left < containment[ 0 ] ) { + pageX = containment[ 0 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top < containment[ 1 ] ) { + pageY = containment[ 1 ] + this.offset.click.top; + } + if ( event.pageX - this.offset.click.left > containment[ 2 ] ) { + pageX = containment[ 2 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top > containment[ 3 ] ) { + pageY = containment[ 3 ] + this.offset.click.top; } - } catch ( err ) { } - } - return true; - }, - /* Pop-up the date picker for a given input field. - * If false returned from beforeShow event handler do not show. - * @param input element - the input field attached to the date picker or - * event - if triggered by focus - */ - _showDatepicker: function( input ) { - input = input.target || input; - if ( input.nodeName.toLowerCase() !== "input" ) { // find from button/image trigger - input = $( "input", input.parentNode )[ 0 ]; - } + if ( o.grid ) { - if ( $.datepicker._isDisabledDatepicker( input ) || $.datepicker._lastInput === input ) { // already here - return; - } + //Check for grid elements set to 0 to prevent divide by 0 error causing invalid + // argument errors in IE (see ticket #6950) + top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY - + this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY; + pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] || + top - this.offset.click.top > containment[ 3 ] ) ? + top : + ( ( top - this.offset.click.top >= containment[ 1 ] ) ? + top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top; - var inst, beforeShow, beforeShowSettings, isFixed, - offset, showAnim, duration; + left = o.grid[ 0 ] ? this.originalPageX + + Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] : + this.originalPageX; + pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] || + left - this.offset.click.left > containment[ 2 ] ) ? + left : + ( ( left - this.offset.click.left >= containment[ 0 ] ) ? + left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left; + } - inst = $.datepicker._getInst( input ); - if ( $.datepicker._curInst && $.datepicker._curInst !== inst ) { - $.datepicker._curInst.dpDiv.stop( true, true ); - if ( inst && $.datepicker._datepickerShowing ) { - $.datepicker._hideDatepicker( $.datepicker._curInst.input[ 0 ] ); + if ( o.axis === "y" ) { + pageX = this.originalPageX; } - } - beforeShow = $.datepicker._get( inst, "beforeShow" ); - beforeShowSettings = beforeShow ? beforeShow.apply( input, [ input, inst ] ) : {}; - if ( beforeShowSettings === false ) { - return; + if ( o.axis === "x" ) { + pageY = this.originalPageY; + } } - datepicker_extendRemove( inst.settings, beforeShowSettings ); - - inst.lastVal = null; - $.datepicker._lastInput = input; - $.datepicker._setDateFromField( inst ); - if ( $.datepicker._inDialog ) { // hide cursor - input.value = ""; - } - if ( !$.datepicker._pos ) { // position below input - $.datepicker._pos = $.datepicker._findPos( input ); - $.datepicker._pos[ 1 ] += input.offsetHeight; // add the height - } + return { + top: ( - isFixed = false; - $( input ).parents().each( function() { - isFixed |= $( this ).css( "position" ) === "fixed"; - return !isFixed; - } ); + // The absolute mouse position + pageY - - offset = { left: $.datepicker._pos[ 0 ], top: $.datepicker._pos[ 1 ] }; - $.datepicker._pos = null; + // Click offset (relative to the element) + this.offset.click.top - - //to avoid flashes on Firefox - inst.dpDiv.empty(); + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top - - // determine sizing offscreen - inst.dpDiv.css( { position: "absolute", display: "block", top: "-1000px" } ); - $.datepicker._updateDatepicker( inst ); + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top + + ( this.cssPosition === "fixed" ? + -this.offset.scroll.top : + ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) + ), + left: ( - // fix width for dynamic number of date pickers - // and adjust position before showing - offset = $.datepicker._checkOffset( inst, offset, isFixed ); - inst.dpDiv.css( { position: ( $.datepicker._inDialog && $.blockUI ? - "static" : ( isFixed ? "fixed" : "absolute" ) ), display: "none", - left: offset.left + "px", top: offset.top + "px" } ); + // The absolute mouse position + pageX - - if ( !inst.inline ) { - showAnim = $.datepicker._get( inst, "showAnim" ); - duration = $.datepicker._get( inst, "duration" ); - inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 ); - $.datepicker._datepickerShowing = true; + // Click offset (relative to the element) + this.offset.click.left - - if ( $.effects && $.effects.effect[ showAnim ] ) { - inst.dpDiv.show( showAnim, $.datepicker._get( inst, "showOptions" ), duration ); - } else { - inst.dpDiv[ showAnim || "show" ]( showAnim ? duration : null ); - } + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left - - if ( $.datepicker._shouldFocusInput( inst ) ) { - inst.input.trigger( "focus" ); - } + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left + + ( this.cssPosition === "fixed" ? + -this.offset.scroll.left : + ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) + ) + }; - $.datepicker._curInst = inst; - } }, - /* Generate the date picker content. */ - _updateDatepicker: function( inst ) { - this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) - datepicker_instActive = inst; // for delegate hover events - inst.dpDiv.empty().append( this._generateHTML( inst ) ); - this._attachHandlers( inst ); - - var origyearshtml, - numMonths = this._getNumberOfMonths( inst ), - cols = numMonths[ 1 ], - width = 17, - activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" ), - onUpdateDatepicker = $.datepicker._get( inst, "onUpdateDatepicker" ); - - if ( activeCell.length > 0 ) { - datepicker_handleMouseover.apply( activeCell.get( 0 ) ); - } - - inst.dpDiv.removeClass( "ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4" ).width( "" ); - if ( cols > 1 ) { - inst.dpDiv.addClass( "ui-datepicker-multi-" + cols ).css( "width", ( width * cols ) + "em" ); + _clear: function() { + this._removeClass( this.helper, "ui-draggable-dragging" ); + if ( this.helper[ 0 ] !== this.element[ 0 ] && !this.cancelHelperRemoval ) { + this.helper.remove(); } - inst.dpDiv[ ( numMonths[ 0 ] !== 1 || numMonths[ 1 ] !== 1 ? "add" : "remove" ) + - "Class" ]( "ui-datepicker-multi" ); - inst.dpDiv[ ( this._get( inst, "isRTL" ) ? "add" : "remove" ) + - "Class" ]( "ui-datepicker-rtl" ); - - if ( inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { - inst.input.trigger( "focus" ); + this.helper = null; + this.cancelHelperRemoval = false; + if ( this.destroyOnClear ) { + this.destroy(); } + }, - // Deffered render of the years select (to avoid flashes on Firefox) - if ( inst.yearshtml ) { - origyearshtml = inst.yearshtml; - setTimeout( function() { + // From now on bulk stuff - mainly helpers - //assure that inst.yearshtml didn't change. - if ( origyearshtml === inst.yearshtml && inst.yearshtml ) { - inst.dpDiv.find( "select.ui-datepicker-year" ).first().replaceWith( inst.yearshtml ); - } - origyearshtml = inst.yearshtml = null; - }, 0 ); - } + _trigger: function( type, event, ui ) { + ui = ui || this._uiHash(); + $.ui.plugin.call( this, type, [ event, ui, this ], true ); - if ( onUpdateDatepicker ) { - onUpdateDatepicker.apply( ( inst.input ? inst.input[ 0 ] : null ), [ inst ] ); + // Absolute position and offset (see #6884 ) have to be recalculated after plugins + if ( /^(drag|start|stop)/.test( type ) ) { + this.positionAbs = this._convertPositionTo( "absolute" ); + ui.offset = this.positionAbs; } + return $.Widget.prototype._trigger.call( this, type, event, ui ); }, - // #6694 - don't focus the input if it's already focused - // this breaks the change event in IE - // Support: IE and jQuery <1.9 - _shouldFocusInput: function( inst ) { - return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); - }, + plugins: {}, - /* Check positioning to remain on screen. */ - _checkOffset: function( inst, offset, isFixed ) { - var dpWidth = inst.dpDiv.outerWidth(), - dpHeight = inst.dpDiv.outerHeight(), - inputWidth = inst.input ? inst.input.outerWidth() : 0, - inputHeight = inst.input ? inst.input.outerHeight() : 0, - viewWidth = document.documentElement.clientWidth + ( isFixed ? 0 : $( document ).scrollLeft() ), - viewHeight = document.documentElement.clientHeight + ( isFixed ? 0 : $( document ).scrollTop() ); + _uiHash: function() { + return { + helper: this.helper, + position: this.position, + originalPosition: this.originalPosition, + offset: this.positionAbs + }; + } - offset.left -= ( this._get( inst, "isRTL" ) ? ( dpWidth - inputWidth ) : 0 ); - offset.left -= ( isFixed && offset.left === inst.input.offset().left ) ? $( document ).scrollLeft() : 0; - offset.top -= ( isFixed && offset.top === ( inst.input.offset().top + inputHeight ) ) ? $( document ).scrollTop() : 0; +} ); - // Now check if datepicker is showing outside window viewport - move to a better place if so. - offset.left -= Math.min( offset.left, ( offset.left + dpWidth > viewWidth && viewWidth > dpWidth ) ? - Math.abs( offset.left + dpWidth - viewWidth ) : 0 ); - offset.top -= Math.min( offset.top, ( offset.top + dpHeight > viewHeight && viewHeight > dpHeight ) ? - Math.abs( dpHeight + inputHeight ) : 0 ); +$.ui.plugin.add( "draggable", "connectToSortable", { + start: function( event, ui, draggable ) { + var uiSortable = $.extend( {}, ui, { + item: draggable.element + } ); - return offset; + draggable.sortables = []; + $( draggable.options.connectToSortable ).each( function() { + var sortable = $( this ).sortable( "instance" ); + + if ( sortable && !sortable.options.disabled ) { + draggable.sortables.push( sortable ); + + // RefreshPositions is called at drag start to refresh the containerCache + // which is used in drag. This ensures it's initialized and synchronized + // with any changes that might have happened on the page since initialization. + sortable.refreshPositions(); + sortable._trigger( "activate", event, uiSortable ); + } + } ); }, + stop: function( event, ui, draggable ) { + var uiSortable = $.extend( {}, ui, { + item: draggable.element + } ); - /* Find an object's position on the screen. */ - _findPos: function( obj ) { - var position, - inst = this._getInst( obj ), - isRTL = this._get( inst, "isRTL" ); + draggable.cancelHelperRemoval = false; - while ( obj && ( obj.type === "hidden" || obj.nodeType !== 1 || $.expr.pseudos.hidden( obj ) ) ) { - obj = obj[ isRTL ? "previousSibling" : "nextSibling" ]; - } + $.each( draggable.sortables, function() { + var sortable = this; - position = $( obj ).offset(); - return [ position.left, position.top ]; - }, + if ( sortable.isOver ) { + sortable.isOver = 0; - /* Hide the date picker from view. - * @param input element - the input field attached to the date picker - */ - _hideDatepicker: function( input ) { - var showAnim, duration, postProcess, onClose, - inst = this._curInst; + // Allow this sortable to handle removing the helper + draggable.cancelHelperRemoval = true; + sortable.cancelHelperRemoval = false; - if ( !inst || ( input && inst !== $.data( input, "datepicker" ) ) ) { - return; - } + // Use _storedCSS To restore properties in the sortable, + // as this also handles revert (#9675) since the draggable + // may have modified them in unexpected ways (#8809) + sortable._storedCSS = { + position: sortable.placeholder.css( "position" ), + top: sortable.placeholder.css( "top" ), + left: sortable.placeholder.css( "left" ) + }; - if ( this._datepickerShowing ) { - showAnim = this._get( inst, "showAnim" ); - duration = this._get( inst, "duration" ); - postProcess = function() { - $.datepicker._tidyDialog( inst ); - }; + sortable._mouseStop( event ); - // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed - if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { - inst.dpDiv.hide( showAnim, $.datepicker._get( inst, "showOptions" ), duration, postProcess ); + // Once drag has ended, the sortable should return to using + // its original helper, not the shared helper from draggable + sortable.options.helper = sortable.options._helper; } else { - inst.dpDiv[ ( showAnim === "slideDown" ? "slideUp" : - ( showAnim === "fadeIn" ? "fadeOut" : "hide" ) ) ]( ( showAnim ? duration : null ), postProcess ); - } - - if ( !showAnim ) { - postProcess(); - } - this._datepickerShowing = false; - onClose = this._get( inst, "onClose" ); - if ( onClose ) { - onClose.apply( ( inst.input ? inst.input[ 0 ] : null ), [ ( inst.input ? inst.input.val() : "" ), inst ] ); - } + // Prevent this Sortable from removing the helper. + // However, don't set the draggable to remove the helper + // either as another connected Sortable may yet handle the removal. + sortable.cancelHelperRemoval = true; - this._lastInput = null; - if ( this._inDialog ) { - this._dialogInput.css( { position: "absolute", left: "0", top: "-100px" } ); - if ( $.blockUI ) { - $.unblockUI(); - $( "body" ).append( this.dpDiv ); - } + sortable._trigger( "deactivate", event, uiSortable ); } - this._inDialog = false; - } + } ); }, + drag: function( event, ui, draggable ) { + $.each( draggable.sortables, function() { + var innermostIntersecting = false, + sortable = this; - /* Tidy up after a dialog display. */ - _tidyDialog: function( inst ) { - inst.dpDiv.removeClass( this._dialogClass ).off( ".ui-datepicker-calendar" ); - }, + // Copy over variables that sortable's _intersectsWith uses + sortable.positionAbs = draggable.positionAbs; + sortable.helperProportions = draggable.helperProportions; + sortable.offset.click = draggable.offset.click; - /* Close date picker if clicked elsewhere. */ - _checkExternalClick: function( event ) { - if ( !$.datepicker._curInst ) { - return; - } + if ( sortable._intersectsWith( sortable.containerCache ) ) { + innermostIntersecting = true; - var $target = $( event.target ), - inst = $.datepicker._getInst( $target[ 0 ] ); + $.each( draggable.sortables, function() { - if ( ( ( $target[ 0 ].id !== $.datepicker._mainDivId && - $target.parents( "#" + $.datepicker._mainDivId ).length === 0 && - !$target.hasClass( $.datepicker.markerClassName ) && - !$target.closest( "." + $.datepicker._triggerClass ).length && - $.datepicker._datepickerShowing && !( $.datepicker._inDialog && $.blockUI ) ) ) || - ( $target.hasClass( $.datepicker.markerClassName ) && $.datepicker._curInst !== inst ) ) { - $.datepicker._hideDatepicker(); - } - }, + // Copy over variables that sortable's _intersectsWith uses + this.positionAbs = draggable.positionAbs; + this.helperProportions = draggable.helperProportions; + this.offset.click = draggable.offset.click; - /* Adjust one of the date sub-fields. */ - _adjustDate: function( id, offset, period ) { - var target = $( id ), - inst = this._getInst( target[ 0 ] ); + if ( this !== sortable && + this._intersectsWith( this.containerCache ) && + $.contains( sortable.element[ 0 ], this.element[ 0 ] ) ) { + innermostIntersecting = false; + } - if ( this._isDisabledDatepicker( target[ 0 ] ) ) { - return; - } - this._adjustInstDate( inst, offset, period ); - this._updateDatepicker( inst ); - }, + return innermostIntersecting; + } ); + } - /* Action for current link. */ - _gotoToday: function( id ) { - var date, - target = $( id ), - inst = this._getInst( target[ 0 ] ); + if ( innermostIntersecting ) { - if ( this._get( inst, "gotoCurrent" ) && inst.currentDay ) { - inst.selectedDay = inst.currentDay; - inst.drawMonth = inst.selectedMonth = inst.currentMonth; - inst.drawYear = inst.selectedYear = inst.currentYear; - } else { - date = new Date(); - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); - } - this._notifyChange( inst ); - this._adjustDate( target ); - }, + // If it intersects, we use a little isOver variable and set it once, + // so that the move-in stuff gets fired only once. + if ( !sortable.isOver ) { + sortable.isOver = 1; - /* Action for selecting a new month/year. */ - _selectMonthYear: function( id, select, period ) { - var target = $( id ), - inst = this._getInst( target[ 0 ] ); + // Store draggable's parent in case we need to reappend to it later. + draggable._parent = ui.helper.parent(); - inst[ "selected" + ( period === "M" ? "Month" : "Year" ) ] = - inst[ "draw" + ( period === "M" ? "Month" : "Year" ) ] = - parseInt( select.options[ select.selectedIndex ].value, 10 ); + sortable.currentItem = ui.helper + .appendTo( sortable.element ) + .data( "ui-sortable-item", true ); - this._notifyChange( inst ); - this._adjustDate( target ); - }, + // Store helper option to later restore it + sortable.options._helper = sortable.options.helper; - /* Action for selecting a day. */ - _selectDay: function( id, month, year, td ) { - var inst, - target = $( id ); + sortable.options.helper = function() { + return ui.helper[ 0 ]; + }; - if ( $( td ).hasClass( this._unselectableClass ) || this._isDisabledDatepicker( target[ 0 ] ) ) { - return; - } + // Fire the start events of the sortable with our passed browser event, + // and our own helper (so it doesn't create a new one) + event.target = sortable.currentItem[ 0 ]; + sortable._mouseCapture( event, true ); + sortable._mouseStart( event, true, true ); - inst = this._getInst( target[ 0 ] ); - inst.selectedDay = inst.currentDay = parseInt( $( "a", td ).attr( "data-date" ) ); - inst.selectedMonth = inst.currentMonth = month; - inst.selectedYear = inst.currentYear = year; - this._selectDate( id, this._formatDate( inst, - inst.currentDay, inst.currentMonth, inst.currentYear ) ); - }, + // Because the browser event is way off the new appended portlet, + // modify necessary variables to reflect the changes + sortable.offset.click.top = draggable.offset.click.top; + sortable.offset.click.left = draggable.offset.click.left; + sortable.offset.parent.left -= draggable.offset.parent.left - + sortable.offset.parent.left; + sortable.offset.parent.top -= draggable.offset.parent.top - + sortable.offset.parent.top; - /* Erase the input field and hide the date picker. */ - _clearDate: function( id ) { - var target = $( id ); - this._selectDate( target, "" ); - }, + draggable._trigger( "toSortable", event ); - /* Update the input field with the selected date. */ - _selectDate: function( id, dateStr ) { - var onSelect, - target = $( id ), - inst = this._getInst( target[ 0 ] ); + // Inform draggable that the helper is in a valid drop zone, + // used solely in the revert option to handle "valid/invalid". + draggable.dropped = sortable.element; - dateStr = ( dateStr != null ? dateStr : this._formatDate( inst ) ); - if ( inst.input ) { - inst.input.val( dateStr ); - } - this._updateAlternate( inst ); + // Need to refreshPositions of all sortables in the case that + // adding to one sortable changes the location of the other sortables (#9675) + $.each( draggable.sortables, function() { + this.refreshPositions(); + } ); - onSelect = this._get( inst, "onSelect" ); - if ( onSelect ) { - onSelect.apply( ( inst.input ? inst.input[ 0 ] : null ), [ dateStr, inst ] ); // trigger custom callback - } else if ( inst.input ) { - inst.input.trigger( "change" ); // fire the change event - } + // Hack so receive/update callbacks work (mostly) + draggable.currentItem = draggable.element; + sortable.fromOutside = draggable; + } - if ( inst.inline ) { - this._updateDatepicker( inst ); - } else { - this._hideDatepicker(); - this._lastInput = inst.input[ 0 ]; - if ( typeof( inst.input[ 0 ] ) !== "object" ) { - inst.input.trigger( "focus" ); // restore focus - } - this._lastInput = null; - } - }, + if ( sortable.currentItem ) { + sortable._mouseDrag( event ); - /* Update any alternate field to synchronise with the main field. */ - _updateAlternate: function( inst ) { - var altFormat, date, dateStr, - altField = this._get( inst, "altField" ); + // Copy the sortable's position because the draggable's can potentially reflect + // a relative position, while sortable is always absolute, which the dragged + // element has now become. (#8809) + ui.position = sortable.position; + } + } else { - if ( altField ) { // update alternate field too - altFormat = this._get( inst, "altFormat" ) || this._get( inst, "dateFormat" ); - date = this._getDate( inst ); - dateStr = this.formatDate( altFormat, date, this._getFormatConfig( inst ) ); - $( document ).find( altField ).val( dateStr ); - } - }, + // If it doesn't intersect with the sortable, and it intersected before, + // we fake the drag stop of the sortable, but make sure it doesn't remove + // the helper by using cancelHelperRemoval. + if ( sortable.isOver ) { - /* Set as beforeShowDay function to prevent selection of weekends. - * @param date Date - the date to customise - * @return [boolean, string] - is this date selectable?, what is its CSS class? - */ - noWeekends: function( date ) { - var day = date.getDay(); - return [ ( day > 0 && day < 6 ), "" ]; - }, + sortable.isOver = 0; + sortable.cancelHelperRemoval = true; - /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. - * @param date Date - the date to get the week for - * @return number - the number of the week within the year that contains this date - */ - iso8601Week: function( date ) { - var time, - checkDate = new Date( date.getTime() ); + // Calling sortable's mouseStop would trigger a revert, + // so revert must be temporarily false until after mouseStop is called. + sortable.options._revert = sortable.options.revert; + sortable.options.revert = false; - // Find Thursday of this week starting on Monday - checkDate.setDate( checkDate.getDate() + 4 - ( checkDate.getDay() || 7 ) ); + sortable._trigger( "out", event, sortable._uiHash( sortable ) ); + sortable._mouseStop( event, true ); - time = checkDate.getTime(); - checkDate.setMonth( 0 ); // Compare with Jan 1 - checkDate.setDate( 1 ); - return Math.floor( Math.round( ( time - checkDate ) / 86400000 ) / 7 ) + 1; - }, + // Restore sortable behaviors that were modfied + // when the draggable entered the sortable area (#9481) + sortable.options.revert = sortable.options._revert; + sortable.options.helper = sortable.options._helper; - /* Parse a string value into a date object. - * See formatDate below for the possible formats. - * - * @param format string - the expected format of the date - * @param value string - the date in the above format - * @param settings Object - attributes include: - * shortYearCutoff number - the cutoff year for determining the century (optional) - * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) - * dayNames string[7] - names of the days from Sunday (optional) - * monthNamesShort string[12] - abbreviated names of the months (optional) - * monthNames string[12] - names of the months (optional) - * @return Date - the extracted date value or null if value is blank - */ - parseDate: function( format, value, settings ) { - if ( format == null || value == null ) { - throw "Invalid arguments"; - } + if ( sortable.placeholder ) { + sortable.placeholder.remove(); + } - value = ( typeof value === "object" ? value.toString() : value + "" ); - if ( value === "" ) { - return null; - } + // Restore and recalculate the draggable's offset considering the sortable + // may have modified them in unexpected ways. (#8809, #10669) + ui.helper.appendTo( draggable._parent ); + draggable._refreshOffsets( event ); + ui.position = draggable._generatePosition( event, true ); - var iFormat, dim, extra, - iValue = 0, - shortYearCutoffTemp = ( settings ? settings.shortYearCutoff : null ) || this._defaults.shortYearCutoff, - shortYearCutoff = ( typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : - new Date().getFullYear() % 100 + parseInt( shortYearCutoffTemp, 10 ) ), - dayNamesShort = ( settings ? settings.dayNamesShort : null ) || this._defaults.dayNamesShort, - dayNames = ( settings ? settings.dayNames : null ) || this._defaults.dayNames, - monthNamesShort = ( settings ? settings.monthNamesShort : null ) || this._defaults.monthNamesShort, - monthNames = ( settings ? settings.monthNames : null ) || this._defaults.monthNames, - year = -1, - month = -1, - day = -1, - doy = -1, - literal = false, - date, + draggable._trigger( "fromSortable", event ); - // Check whether a format character is doubled - lookAhead = function( match ) { - var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); - if ( matches ) { - iFormat++; - } - return matches; - }, + // Inform draggable that the helper is no longer in a valid drop zone + draggable.dropped = false; - // Extract a number from the string value - getNumber = function( match ) { - var isDoubled = lookAhead( match ), - size = ( match === "@" ? 14 : ( match === "!" ? 20 : - ( match === "y" && isDoubled ? 4 : ( match === "o" ? 3 : 2 ) ) ) ), - minSize = ( match === "y" ? size : 1 ), - digits = new RegExp( "^\\d{" + minSize + "," + size + "}" ), - num = value.substring( iValue ).match( digits ); - if ( !num ) { - throw "Missing number at position " + iValue; + // Need to refreshPositions of all sortables just in case removing + // from one sortable changes the location of other sortables (#9675) + $.each( draggable.sortables, function() { + this.refreshPositions(); + } ); } - iValue += num[ 0 ].length; - return parseInt( num[ 0 ], 10 ); - }, + } + } ); + } +} ); - // Extract a name from the string value and convert to an index - getName = function( match, shortNames, longNames ) { - var index = -1, - names = $.map( lookAhead( match ) ? longNames : shortNames, function( v, k ) { - return [ [ k, v ] ]; - } ).sort( function( a, b ) { - return -( a[ 1 ].length - b[ 1 ].length ); - } ); - - $.each( names, function( i, pair ) { - var name = pair[ 1 ]; - if ( value.substr( iValue, name.length ).toLowerCase() === name.toLowerCase() ) { - index = pair[ 0 ]; - iValue += name.length; - return false; - } - } ); - if ( index !== -1 ) { - return index + 1; - } else { - throw "Unknown name at position " + iValue; - } - }, - - // Confirm that a literal character matches the string value - checkLiteral = function() { - if ( value.charAt( iValue ) !== format.charAt( iFormat ) ) { - throw "Unexpected literal at position " + iValue; - } - iValue++; - }; +$.ui.plugin.add( "draggable", "cursor", { + start: function( event, ui, instance ) { + var t = $( "body" ), + o = instance.options; - for ( iFormat = 0; iFormat < format.length; iFormat++ ) { - if ( literal ) { - if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { - literal = false; - } else { - checkLiteral(); - } - } else { - switch ( format.charAt( iFormat ) ) { - case "d": - day = getNumber( "d" ); - break; - case "D": - getName( "D", dayNamesShort, dayNames ); - break; - case "o": - doy = getNumber( "o" ); - break; - case "m": - month = getNumber( "m" ); - break; - case "M": - month = getName( "M", monthNamesShort, monthNames ); - break; - case "y": - year = getNumber( "y" ); - break; - case "@": - date = new Date( getNumber( "@" ) ); - year = date.getFullYear(); - month = date.getMonth() + 1; - day = date.getDate(); - break; - case "!": - date = new Date( ( getNumber( "!" ) - this._ticksTo1970 ) / 10000 ); - year = date.getFullYear(); - month = date.getMonth() + 1; - day = date.getDate(); - break; - case "'": - if ( lookAhead( "'" ) ) { - checkLiteral(); - } else { - literal = true; - } - break; - default: - checkLiteral(); - } - } + if ( t.css( "cursor" ) ) { + o._cursor = t.css( "cursor" ); } - - if ( iValue < value.length ) { - extra = value.substr( iValue ); - if ( !/^\s+/.test( extra ) ) { - throw "Extra/unparsed characters found in date: " + extra; - } + t.css( "cursor", o.cursor ); + }, + stop: function( event, ui, instance ) { + var o = instance.options; + if ( o._cursor ) { + $( "body" ).css( "cursor", o._cursor ); } + } +} ); - if ( year === -1 ) { - year = new Date().getFullYear(); - } else if ( year < 100 ) { - year += new Date().getFullYear() - new Date().getFullYear() % 100 + - ( year <= shortYearCutoff ? 0 : -100 ); +$.ui.plugin.add( "draggable", "opacity", { + start: function( event, ui, instance ) { + var t = $( ui.helper ), + o = instance.options; + if ( t.css( "opacity" ) ) { + o._opacity = t.css( "opacity" ); } + t.css( "opacity", o.opacity ); + }, + stop: function( event, ui, instance ) { + var o = instance.options; + if ( o._opacity ) { + $( ui.helper ).css( "opacity", o._opacity ); + } + } +} ); - if ( doy > -1 ) { - month = 1; - day = doy; - do { - dim = this._getDaysInMonth( year, month - 1 ); - if ( day <= dim ) { - break; - } - month++; - day -= dim; - } while ( true ); +$.ui.plugin.add( "draggable", "scroll", { + start: function( event, ui, i ) { + if ( !i.scrollParentNotHidden ) { + i.scrollParentNotHidden = i.helper.scrollParent( false ); } - date = this._daylightSavingAdjust( new Date( year, month - 1, day ) ); - if ( date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day ) { - throw "Invalid date"; // E.g. 31/02/00 + if ( i.scrollParentNotHidden[ 0 ] !== i.document[ 0 ] && + i.scrollParentNotHidden[ 0 ].tagName !== "HTML" ) { + i.overflowOffset = i.scrollParentNotHidden.offset(); } - return date; }, + drag: function( event, ui, i ) { - /* Standard date formats. */ - ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) - COOKIE: "D, dd M yy", - ISO_8601: "yy-mm-dd", - RFC_822: "D, d M y", - RFC_850: "DD, dd-M-y", - RFC_1036: "D, d M y", - RFC_1123: "D, d M yy", - RFC_2822: "D, d M yy", - RSS: "D, d M y", // RFC 822 - TICKS: "!", - TIMESTAMP: "@", - W3C: "yy-mm-dd", // ISO 8601 + var o = i.options, + scrolled = false, + scrollParent = i.scrollParentNotHidden[ 0 ], + document = i.document[ 0 ]; - _ticksTo1970: ( ( ( 1970 - 1 ) * 365 + Math.floor( 1970 / 4 ) - Math.floor( 1970 / 100 ) + - Math.floor( 1970 / 400 ) ) * 24 * 60 * 60 * 10000000 ), + if ( scrollParent !== document && scrollParent.tagName !== "HTML" ) { + if ( !o.axis || o.axis !== "x" ) { + if ( ( i.overflowOffset.top + scrollParent.offsetHeight ) - event.pageY < + o.scrollSensitivity ) { + scrollParent.scrollTop = scrolled = scrollParent.scrollTop + o.scrollSpeed; + } else if ( event.pageY - i.overflowOffset.top < o.scrollSensitivity ) { + scrollParent.scrollTop = scrolled = scrollParent.scrollTop - o.scrollSpeed; + } + } - /* Format a date object into a string value. - * The format can be combinations of the following: - * d - day of month (no leading zero) - * dd - day of month (two digit) - * o - day of year (no leading zeros) - * oo - day of year (three digit) - * D - day name short - * DD - day name long - * m - month of year (no leading zero) - * mm - month of year (two digit) - * M - month name short - * MM - month name long - * y - year (two digit) - * yy - year (four digit) - * @ - Unix timestamp (ms since 01/01/1970) - * ! - Windows ticks (100ns since 01/01/0001) - * "..." - literal text - * '' - single quote - * - * @param format string - the desired format of the date - * @param date Date - the date value to format - * @param settings Object - attributes include: - * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) - * dayNames string[7] - names of the days from Sunday (optional) - * monthNamesShort string[12] - abbreviated names of the months (optional) - * monthNames string[12] - names of the months (optional) - * @return string - the date in the above format - */ - formatDate: function( format, date, settings ) { - if ( !date ) { - return ""; - } + if ( !o.axis || o.axis !== "y" ) { + if ( ( i.overflowOffset.left + scrollParent.offsetWidth ) - event.pageX < + o.scrollSensitivity ) { + scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft + o.scrollSpeed; + } else if ( event.pageX - i.overflowOffset.left < o.scrollSensitivity ) { + scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft - o.scrollSpeed; + } + } - var iFormat, - dayNamesShort = ( settings ? settings.dayNamesShort : null ) || this._defaults.dayNamesShort, - dayNames = ( settings ? settings.dayNames : null ) || this._defaults.dayNames, - monthNamesShort = ( settings ? settings.monthNamesShort : null ) || this._defaults.monthNamesShort, - monthNames = ( settings ? settings.monthNames : null ) || this._defaults.monthNames, + } else { - // Check whether a format character is doubled - lookAhead = function( match ) { - var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); - if ( matches ) { - iFormat++; + if ( !o.axis || o.axis !== "x" ) { + if ( event.pageY - $( document ).scrollTop() < o.scrollSensitivity ) { + scrolled = $( document ).scrollTop( $( document ).scrollTop() - o.scrollSpeed ); + } else if ( $( window ).height() - ( event.pageY - $( document ).scrollTop() ) < + o.scrollSensitivity ) { + scrolled = $( document ).scrollTop( $( document ).scrollTop() + o.scrollSpeed ); } - return matches; - }, + } - // Format a number, with leading zero if necessary - formatNumber = function( match, value, len ) { - var num = "" + value; - if ( lookAhead( match ) ) { - while ( num.length < len ) { - num = "0" + num; - } + if ( !o.axis || o.axis !== "y" ) { + if ( event.pageX - $( document ).scrollLeft() < o.scrollSensitivity ) { + scrolled = $( document ).scrollLeft( + $( document ).scrollLeft() - o.scrollSpeed + ); + } else if ( $( window ).width() - ( event.pageX - $( document ).scrollLeft() ) < + o.scrollSensitivity ) { + scrolled = $( document ).scrollLeft( + $( document ).scrollLeft() + o.scrollSpeed + ); } - return num; - }, + } - // Format a name, short or long as requested - formatName = function( match, value, shortNames, longNames ) { - return ( lookAhead( match ) ? longNames[ value ] : shortNames[ value ] ); - }, - output = "", - literal = false; - - if ( date ) { - for ( iFormat = 0; iFormat < format.length; iFormat++ ) { - if ( literal ) { - if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { - literal = false; - } else { - output += format.charAt( iFormat ); - } - } else { - switch ( format.charAt( iFormat ) ) { - case "d": - output += formatNumber( "d", date.getDate(), 2 ); - break; - case "D": - output += formatName( "D", date.getDay(), dayNamesShort, dayNames ); - break; - case "o": - output += formatNumber( "o", - Math.round( ( new Date( date.getFullYear(), date.getMonth(), date.getDate() ).getTime() - new Date( date.getFullYear(), 0, 0 ).getTime() ) / 86400000 ), 3 ); - break; - case "m": - output += formatNumber( "m", date.getMonth() + 1, 2 ); - break; - case "M": - output += formatName( "M", date.getMonth(), monthNamesShort, monthNames ); - break; - case "y": - output += ( lookAhead( "y" ) ? date.getFullYear() : - ( date.getFullYear() % 100 < 10 ? "0" : "" ) + date.getFullYear() % 100 ); - break; - case "@": - output += date.getTime(); - break; - case "!": - output += date.getTime() * 10000 + this._ticksTo1970; - break; - case "'": - if ( lookAhead( "'" ) ) { - output += "'"; - } else { - literal = true; - } - break; - default: - output += format.charAt( iFormat ); - } - } - } } - return output; - }, - - /* Extract all possible characters from the date format. */ - _possibleChars: function( format ) { - var iFormat, - chars = "", - literal = false, - - // Check whether a format character is doubled - lookAhead = function( match ) { - var matches = ( iFormat + 1 < format.length && format.charAt( iFormat + 1 ) === match ); - if ( matches ) { - iFormat++; - } - return matches; - }; - for ( iFormat = 0; iFormat < format.length; iFormat++ ) { - if ( literal ) { - if ( format.charAt( iFormat ) === "'" && !lookAhead( "'" ) ) { - literal = false; - } else { - chars += format.charAt( iFormat ); - } - } else { - switch ( format.charAt( iFormat ) ) { - case "d": case "m": case "y": case "@": - chars += "0123456789"; - break; - case "D": case "M": - return null; // Accept anything - case "'": - if ( lookAhead( "'" ) ) { - chars += "'"; - } else { - literal = true; - } - break; - default: - chars += format.charAt( iFormat ); - } - } + if ( scrolled !== false && $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( i, event ); } - return chars; - }, - /* Get a setting value, defaulting if necessary. */ - _get: function( inst, name ) { - return inst.settings[ name ] !== undefined ? - inst.settings[ name ] : this._defaults[ name ]; - }, + } +} ); - /* Parse existing date and initialise date picker. */ - _setDateFromField: function( inst, noDefault ) { - if ( inst.input.val() === inst.lastVal ) { - return; - } +$.ui.plugin.add( "draggable", "snap", { + start: function( event, ui, i ) { - var dateFormat = this._get( inst, "dateFormat" ), - dates = inst.lastVal = inst.input ? inst.input.val() : null, - defaultDate = this._getDefaultDate( inst ), - date = defaultDate, - settings = this._getFormatConfig( inst ); + var o = i.options; - try { - date = this.parseDate( dateFormat, dates, settings ) || defaultDate; - } catch ( event ) { - dates = ( noDefault ? "" : dates ); - } - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); - inst.currentDay = ( dates ? date.getDate() : 0 ); - inst.currentMonth = ( dates ? date.getMonth() : 0 ); - inst.currentYear = ( dates ? date.getFullYear() : 0 ); - this._adjustInstDate( inst ); - }, + i.snapElements = []; + + $( o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap ) + .each( function() { + var $t = $( this ), + $o = $t.offset(); + if ( this !== i.element[ 0 ] ) { + i.snapElements.push( { + item: this, + width: $t.outerWidth(), height: $t.outerHeight(), + top: $o.top, left: $o.left + } ); + } + } ); - /* Retrieve the default date shown on opening. */ - _getDefaultDate: function( inst ) { - return this._restrictMinMax( inst, - this._determineDate( inst, this._get( inst, "defaultDate" ), new Date() ) ); }, + drag: function( event, ui, inst ) { - /* A date may be specified as an exact value or a relative one. */ - _determineDate: function( inst, date, defaultDate ) { - var offsetNumeric = function( offset ) { - var date = new Date(); - date.setDate( date.getDate() + offset ); - return date; - }, - offsetString = function( offset ) { - try { - return $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ), - offset, $.datepicker._getFormatConfig( inst ) ); - } catch ( e ) { + var ts, bs, ls, rs, l, r, t, b, i, first, + o = inst.options, + d = o.snapTolerance, + x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, + y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; - // Ignore - } + for ( i = inst.snapElements.length - 1; i >= 0; i-- ) { - var date = ( offset.toLowerCase().match( /^c/ ) ? - $.datepicker._getDate( inst ) : null ) || new Date(), - year = date.getFullYear(), - month = date.getMonth(), - day = date.getDate(), - pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, - matches = pattern.exec( offset ); + l = inst.snapElements[ i ].left - inst.margins.left; + r = l + inst.snapElements[ i ].width; + t = inst.snapElements[ i ].top - inst.margins.top; + b = t + inst.snapElements[ i ].height; - while ( matches ) { - switch ( matches[ 2 ] || "d" ) { - case "d" : case "D" : - day += parseInt( matches[ 1 ], 10 ); break; - case "w" : case "W" : - day += parseInt( matches[ 1 ], 10 ) * 7; break; - case "m" : case "M" : - month += parseInt( matches[ 1 ], 10 ); - day = Math.min( day, $.datepicker._getDaysInMonth( year, month ) ); - break; - case "y": case "Y" : - year += parseInt( matches[ 1 ], 10 ); - day = Math.min( day, $.datepicker._getDaysInMonth( year, month ) ); - break; + if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || + !$.contains( inst.snapElements[ i ].item.ownerDocument, + inst.snapElements[ i ].item ) ) { + if ( inst.snapElements[ i ].snapping ) { + if ( inst.options.snap.release ) { + inst.options.snap.release.call( + inst.element, + event, + $.extend( inst._uiHash(), { snapItem: inst.snapElements[ i ].item } ) + ); } - matches = pattern.exec( offset ); } - return new Date( year, month, day ); - }, - newDate = ( date == null || date === "" ? defaultDate : ( typeof date === "string" ? offsetString( date ) : - ( typeof date === "number" ? ( isNaN( date ) ? defaultDate : offsetNumeric( date ) ) : new Date( date.getTime() ) ) ) ); + inst.snapElements[ i ].snapping = false; + continue; + } - newDate = ( newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate ); - if ( newDate ) { - newDate.setHours( 0 ); - newDate.setMinutes( 0 ); - newDate.setSeconds( 0 ); - newDate.setMilliseconds( 0 ); - } - return this._daylightSavingAdjust( newDate ); - }, + if ( o.snapMode !== "inner" ) { + ts = Math.abs( t - y2 ) <= d; + bs = Math.abs( b - y1 ) <= d; + ls = Math.abs( l - x2 ) <= d; + rs = Math.abs( r - x1 ) <= d; + if ( ts ) { + ui.position.top = inst._convertPositionTo( "relative", { + top: t - inst.helperProportions.height, + left: 0 + } ).top; + } + if ( bs ) { + ui.position.top = inst._convertPositionTo( "relative", { + top: b, + left: 0 + } ).top; + } + if ( ls ) { + ui.position.left = inst._convertPositionTo( "relative", { + top: 0, + left: l - inst.helperProportions.width + } ).left; + } + if ( rs ) { + ui.position.left = inst._convertPositionTo( "relative", { + top: 0, + left: r + } ).left; + } + } + + first = ( ts || bs || ls || rs ); + + if ( o.snapMode !== "outer" ) { + ts = Math.abs( t - y1 ) <= d; + bs = Math.abs( b - y2 ) <= d; + ls = Math.abs( l - x1 ) <= d; + rs = Math.abs( r - x2 ) <= d; + if ( ts ) { + ui.position.top = inst._convertPositionTo( "relative", { + top: t, + left: 0 + } ).top; + } + if ( bs ) { + ui.position.top = inst._convertPositionTo( "relative", { + top: b - inst.helperProportions.height, + left: 0 + } ).top; + } + if ( ls ) { + ui.position.left = inst._convertPositionTo( "relative", { + top: 0, + left: l + } ).left; + } + if ( rs ) { + ui.position.left = inst._convertPositionTo( "relative", { + top: 0, + left: r - inst.helperProportions.width + } ).left; + } + } + + if ( !inst.snapElements[ i ].snapping && ( ts || bs || ls || rs || first ) ) { + if ( inst.options.snap.snap ) { + inst.options.snap.snap.call( + inst.element, + event, + $.extend( inst._uiHash(), { + snapItem: inst.snapElements[ i ].item + } ) ); + } + } + inst.snapElements[ i ].snapping = ( ts || bs || ls || rs || first ); - /* Handle switch to/from daylight saving. - * Hours may be non-zero on daylight saving cut-over: - * > 12 when midnight changeover, but then cannot generate - * midnight datetime, so jump to 1AM, otherwise reset. - * @param date (Date) the date to check - * @return (Date) the corrected date - */ - _daylightSavingAdjust: function( date ) { - if ( !date ) { - return null; } - date.setHours( date.getHours() > 12 ? date.getHours() + 2 : 0 ); - return date; - }, - /* Set the date(s) directly. */ - _setDate: function( inst, date, noChange ) { - var clear = !date, - origMonth = inst.selectedMonth, - origYear = inst.selectedYear, - newDate = this._restrictMinMax( inst, this._determineDate( inst, date, new Date() ) ); + } +} ); - inst.selectedDay = inst.currentDay = newDate.getDate(); - inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); - inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); - if ( ( origMonth !== inst.selectedMonth || origYear !== inst.selectedYear ) && !noChange ) { - this._notifyChange( inst ); +$.ui.plugin.add( "draggable", "stack", { + start: function( event, ui, instance ) { + var min, + o = instance.options, + group = $.makeArray( $( o.stack ) ).sort( function( a, b ) { + return ( parseInt( $( a ).css( "zIndex" ), 10 ) || 0 ) - + ( parseInt( $( b ).css( "zIndex" ), 10 ) || 0 ); + } ); + + if ( !group.length ) { + return; } - this._adjustInstDate( inst ); - if ( inst.input ) { - inst.input.val( clear ? "" : this._formatDate( inst ) ); + + min = parseInt( $( group[ 0 ] ).css( "zIndex" ), 10 ) || 0; + $( group ).each( function( i ) { + $( this ).css( "zIndex", min + i ); + } ); + this.css( "zIndex", ( min + group.length ) ); + } +} ); + +$.ui.plugin.add( "draggable", "zIndex", { + start: function( event, ui, instance ) { + var t = $( ui.helper ), + o = instance.options; + + if ( t.css( "zIndex" ) ) { + o._zIndex = t.css( "zIndex" ); } + t.css( "zIndex", o.zIndex ); }, + stop: function( event, ui, instance ) { + var o = instance.options; - /* Retrieve the date(s) directly. */ - _getDate: function( inst ) { - var startDate = ( !inst.currentYear || ( inst.input && inst.input.val() === "" ) ? null : - this._daylightSavingAdjust( new Date( - inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); - return startDate; + if ( o._zIndex ) { + $( ui.helper ).css( "zIndex", o._zIndex ); + } + } +} ); + +var widgetsDraggable = $.ui.draggable; + + +/*! + * jQuery UI Resizable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Resizable +//>>group: Interactions +//>>description: Enables resize functionality for any element. +//>>docs: http://api.jqueryui.com/resizable/ +//>>demos: http://jqueryui.com/resizable/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/resizable.css +//>>css.theme: ../../themes/base/theme.css + + +$.widget( "ui.resizable", $.ui.mouse, { + version: "1.13.2", + widgetEventPrefix: "resize", + options: { + alsoResize: false, + animate: false, + animateDuration: "slow", + animateEasing: "swing", + aspectRatio: false, + autoHide: false, + classes: { + "ui-resizable-se": "ui-icon ui-icon-gripsmall-diagonal-se" + }, + containment: false, + ghost: false, + grid: false, + handles: "e,s,se", + helper: false, + maxHeight: null, + maxWidth: null, + minHeight: 10, + minWidth: 10, + + // See #7960 + zIndex: 90, + + // Callbacks + resize: null, + start: null, + stop: null }, - /* Attach the onxxx handlers. These are declared statically so - * they work with static code transformers like Caja. - */ - _attachHandlers: function( inst ) { - var stepMonths = this._get( inst, "stepMonths" ), - id = "#" + inst.id.replace( /\\\\/g, "\\" ); - inst.dpDiv.find( "[data-handler]" ).map( function() { - var handler = { - prev: function() { - $.datepicker._adjustDate( id, -stepMonths, "M" ); - }, - next: function() { - $.datepicker._adjustDate( id, +stepMonths, "M" ); - }, - hide: function() { - $.datepicker._hideDatepicker(); - }, - today: function() { - $.datepicker._gotoToday( id ); - }, - selectDay: function() { - $.datepicker._selectDay( id, +this.getAttribute( "data-month" ), +this.getAttribute( "data-year" ), this ); - return false; - }, - selectMonth: function() { - $.datepicker._selectMonthYear( id, this, "M" ); - return false; - }, - selectYear: function() { - $.datepicker._selectMonthYear( id, this, "Y" ); - return false; - } - }; - $( this ).on( this.getAttribute( "data-event" ), handler[ this.getAttribute( "data-handler" ) ] ); - } ); + _num: function( value ) { + return parseFloat( value ) || 0; }, - /* Generate the HTML for the current state of the date picker. */ - _generateHTML: function( inst ) { - var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, - controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, - monthNames, monthNamesShort, beforeShowDay, showOtherMonths, - selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, - cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, - printDate, dRow, tbody, daySettings, otherMonth, unselectable, - tempDate = new Date(), - today = this._daylightSavingAdjust( - new Date( tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() ) ), // clear time - isRTL = this._get( inst, "isRTL" ), - showButtonPanel = this._get( inst, "showButtonPanel" ), - hideIfNoPrevNext = this._get( inst, "hideIfNoPrevNext" ), - navigationAsDateFormat = this._get( inst, "navigationAsDateFormat" ), - numMonths = this._getNumberOfMonths( inst ), - showCurrentAtPos = this._get( inst, "showCurrentAtPos" ), - stepMonths = this._get( inst, "stepMonths" ), - isMultiMonth = ( numMonths[ 0 ] !== 1 || numMonths[ 1 ] !== 1 ), - currentDate = this._daylightSavingAdjust( ( !inst.currentDay ? new Date( 9999, 9, 9 ) : - new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ), - minDate = this._getMinMaxDate( inst, "min" ), - maxDate = this._getMinMaxDate( inst, "max" ), - drawMonth = inst.drawMonth - showCurrentAtPos, - drawYear = inst.drawYear; + _isNumber: function( value ) { + return !isNaN( parseFloat( value ) ); + }, - if ( drawMonth < 0 ) { - drawMonth += 12; - drawYear--; - } - if ( maxDate ) { - maxDraw = this._daylightSavingAdjust( new Date( maxDate.getFullYear(), - maxDate.getMonth() - ( numMonths[ 0 ] * numMonths[ 1 ] ) + 1, maxDate.getDate() ) ); - maxDraw = ( minDate && maxDraw < minDate ? minDate : maxDraw ); - while ( this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 ) ) > maxDraw ) { - drawMonth--; - if ( drawMonth < 0 ) { - drawMonth = 11; - drawYear--; - } - } + _hasScroll: function( el, a ) { + + if ( $( el ).css( "overflow" ) === "hidden" ) { + return false; } - inst.drawMonth = drawMonth; - inst.drawYear = drawYear; - prevText = this._get( inst, "prevText" ); - prevText = ( !navigationAsDateFormat ? prevText : this.formatDate( prevText, - this._daylightSavingAdjust( new Date( drawYear, drawMonth - stepMonths, 1 ) ), - this._getFormatConfig( inst ) ) ); + var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", + has = false; - if ( this._canAdjustMonth( inst, -1, drawYear, drawMonth ) ) { - prev = $( "<a>" ) - .attr( { - "class": "ui-datepicker-prev ui-corner-all", - "data-handler": "prev", - "data-event": "click", - title: prevText - } ) - .append( - $( "<span>" ) - .addClass( "ui-icon ui-icon-circle-triangle-" + - ( isRTL ? "e" : "w" ) ) - .text( prevText ) - )[ 0 ].outerHTML; - } else if ( hideIfNoPrevNext ) { - prev = ""; - } else { - prev = $( "<a>" ) - .attr( { - "class": "ui-datepicker-prev ui-corner-all ui-state-disabled", - title: prevText - } ) - .append( - $( "<span>" ) - .addClass( "ui-icon ui-icon-circle-triangle-" + - ( isRTL ? "e" : "w" ) ) - .text( prevText ) - )[ 0 ].outerHTML; + if ( el[ scroll ] > 0 ) { + return true; } - nextText = this._get( inst, "nextText" ); - nextText = ( !navigationAsDateFormat ? nextText : this.formatDate( nextText, - this._daylightSavingAdjust( new Date( drawYear, drawMonth + stepMonths, 1 ) ), - this._getFormatConfig( inst ) ) ); + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + try { + el[ scroll ] = 1; + has = ( el[ scroll ] > 0 ); + el[ scroll ] = 0; + } catch ( e ) { - if ( this._canAdjustMonth( inst, +1, drawYear, drawMonth ) ) { - next = $( "<a>" ) - .attr( { - "class": "ui-datepicker-next ui-corner-all", - "data-handler": "next", - "data-event": "click", - title: nextText - } ) - .append( - $( "<span>" ) - .addClass( "ui-icon ui-icon-circle-triangle-" + - ( isRTL ? "w" : "e" ) ) - .text( nextText ) - )[ 0 ].outerHTML; - } else if ( hideIfNoPrevNext ) { - next = ""; - } else { - next = $( "<a>" ) - .attr( { - "class": "ui-datepicker-next ui-corner-all ui-state-disabled", - title: nextText + // `el` might be a string, then setting `scroll` will throw + // an error in strict mode; ignore it. + } + return has; + }, + + _create: function() { + + var margins, + o = this.options, + that = this; + this._addClass( "ui-resizable" ); + + $.extend( this, { + _aspectRatio: !!( o.aspectRatio ), + aspectRatio: o.aspectRatio, + originalElement: this.element, + _proportionallyResizeElements: [], + _helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null + } ); + + // Wrap the element if it cannot hold child nodes + if ( this.element[ 0 ].nodeName.match( /^(canvas|textarea|input|select|button|img)$/i ) ) { + + this.element.wrap( + $( "<div class='ui-wrapper'></div>" ).css( { + overflow: "hidden", + position: this.element.css( "position" ), + width: this.element.outerWidth(), + height: this.element.outerHeight(), + top: this.element.css( "top" ), + left: this.element.css( "left" ) } ) - .append( - $( "<span>" ) - .attr( "class", "ui-icon ui-icon-circle-triangle-" + - ( isRTL ? "w" : "e" ) ) - .text( nextText ) - )[ 0 ].outerHTML; + ); + + this.element = this.element.parent().data( + "ui-resizable", this.element.resizable( "instance" ) + ); + + this.elementIsWrapper = true; + + margins = { + marginTop: this.originalElement.css( "marginTop" ), + marginRight: this.originalElement.css( "marginRight" ), + marginBottom: this.originalElement.css( "marginBottom" ), + marginLeft: this.originalElement.css( "marginLeft" ) + }; + + this.element.css( margins ); + this.originalElement.css( "margin", 0 ); + + // support: Safari + // Prevent Safari textarea resize + this.originalResizeStyle = this.originalElement.css( "resize" ); + this.originalElement.css( "resize", "none" ); + + this._proportionallyResizeElements.push( this.originalElement.css( { + position: "static", + zoom: 1, + display: "block" + } ) ); + + // Support: IE9 + // avoid IE jump (hard set the margin) + this.originalElement.css( margins ); + + this._proportionallyResize(); } - currentText = this._get( inst, "currentText" ); - gotoDate = ( this._get( inst, "gotoCurrent" ) && inst.currentDay ? currentDate : today ); - currentText = ( !navigationAsDateFormat ? currentText : - this.formatDate( currentText, gotoDate, this._getFormatConfig( inst ) ) ); + this._setupHandles(); - controls = ""; - if ( !inst.inline ) { - controls = $( "<button>" ) - .attr( { - type: "button", - "class": "ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all", - "data-handler": "hide", - "data-event": "click" + if ( o.autoHide ) { + $( this.element ) + .on( "mouseenter", function() { + if ( o.disabled ) { + return; + } + that._removeClass( "ui-resizable-autohide" ); + that._handles.show(); } ) - .text( this._get( inst, "closeText" ) )[ 0 ].outerHTML; + .on( "mouseleave", function() { + if ( o.disabled ) { + return; + } + if ( !that.resizing ) { + that._addClass( "ui-resizable-autohide" ); + that._handles.hide(); + } + } ); } - buttonPanel = ""; - if ( showButtonPanel ) { - buttonPanel = $( "<div class='ui-datepicker-buttonpane ui-widget-content'>" ) - .append( isRTL ? controls : "" ) - .append( this._isInRange( inst, gotoDate ) ? - $( "<button>" ) - .attr( { - type: "button", - "class": "ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all", - "data-handler": "today", - "data-event": "click" - } ) - .text( currentText ) : - "" ) - .append( isRTL ? "" : controls )[ 0 ].outerHTML; + this._mouseInit(); + }, + + _destroy: function() { + + this._mouseDestroy(); + this._addedHandles.remove(); + + var wrapper, + _destroy = function( exp ) { + $( exp ) + .removeData( "resizable" ) + .removeData( "ui-resizable" ) + .off( ".resizable" ); + }; + + // TODO: Unwrap at same DOM position + if ( this.elementIsWrapper ) { + _destroy( this.element ); + wrapper = this.element; + this.originalElement.css( { + position: wrapper.css( "position" ), + width: wrapper.outerWidth(), + height: wrapper.outerHeight(), + top: wrapper.css( "top" ), + left: wrapper.css( "left" ) + } ).insertAfter( wrapper ); + wrapper.remove(); } - firstDay = parseInt( this._get( inst, "firstDay" ), 10 ); - firstDay = ( isNaN( firstDay ) ? 0 : firstDay ); + this.originalElement.css( "resize", this.originalResizeStyle ); + _destroy( this.originalElement ); - showWeek = this._get( inst, "showWeek" ); - dayNames = this._get( inst, "dayNames" ); - dayNamesMin = this._get( inst, "dayNamesMin" ); - monthNames = this._get( inst, "monthNames" ); - monthNamesShort = this._get( inst, "monthNamesShort" ); - beforeShowDay = this._get( inst, "beforeShowDay" ); - showOtherMonths = this._get( inst, "showOtherMonths" ); - selectOtherMonths = this._get( inst, "selectOtherMonths" ); - defaultDate = this._getDefaultDate( inst ); - html = ""; + return this; + }, - for ( row = 0; row < numMonths[ 0 ]; row++ ) { - group = ""; - this.maxRows = 4; - for ( col = 0; col < numMonths[ 1 ]; col++ ) { - selectedDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, inst.selectedDay ) ); - cornerClass = " ui-corner-all"; - calender = ""; - if ( isMultiMonth ) { - calender += "<div class='ui-datepicker-group"; - if ( numMonths[ 1 ] > 1 ) { - switch ( col ) { - case 0: calender += " ui-datepicker-group-first"; - cornerClass = " ui-corner-" + ( isRTL ? "right" : "left" ); break; - case numMonths[ 1 ] - 1: calender += " ui-datepicker-group-last"; - cornerClass = " ui-corner-" + ( isRTL ? "left" : "right" ); break; - default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break; - } - } - calender += "'>"; - } - calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + - ( /all|left/.test( cornerClass ) && row === 0 ? ( isRTL ? next : prev ) : "" ) + - ( /all|right/.test( cornerClass ) && row === 0 ? ( isRTL ? prev : next ) : "" ) + - this._generateMonthYearHeader( inst, drawMonth, drawYear, minDate, maxDate, - row > 0 || col > 0, monthNames, monthNamesShort ) + // draw month headers - "</div><table class='ui-datepicker-calendar'><thead>" + - "<tr>"; - thead = ( showWeek ? "<th class='ui-datepicker-week-col'>" + this._get( inst, "weekHeader" ) + "</th>" : "" ); - for ( dow = 0; dow < 7; dow++ ) { // days of the week - day = ( dow + firstDay ) % 7; - thead += "<th scope='col'" + ( ( dow + firstDay + 6 ) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "" ) + ">" + - "<span title='" + dayNames[ day ] + "'>" + dayNamesMin[ day ] + "</span></th>"; - } - calender += thead + "</tr></thead><tbody>"; - daysInMonth = this._getDaysInMonth( drawYear, drawMonth ); - if ( drawYear === inst.selectedYear && drawMonth === inst.selectedMonth ) { - inst.selectedDay = Math.min( inst.selectedDay, daysInMonth ); - } - leadDays = ( this._getFirstDayOfMonth( drawYear, drawMonth ) - firstDay + 7 ) % 7; - curRows = Math.ceil( ( leadDays + daysInMonth ) / 7 ); // calculate the number of rows to generate - numRows = ( isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows ); //If multiple months, use the higher number of rows (see #7043) - this.maxRows = numRows; - printDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 - leadDays ) ); - for ( dRow = 0; dRow < numRows; dRow++ ) { // create date picker rows - calender += "<tr>"; - tbody = ( !showWeek ? "" : "<td class='ui-datepicker-week-col'>" + - this._get( inst, "calculateWeek" )( printDate ) + "</td>" ); - for ( dow = 0; dow < 7; dow++ ) { // create date picker days - daySettings = ( beforeShowDay ? - beforeShowDay.apply( ( inst.input ? inst.input[ 0 ] : null ), [ printDate ] ) : [ true, "" ] ); - otherMonth = ( printDate.getMonth() !== drawMonth ); - unselectable = ( otherMonth && !selectOtherMonths ) || !daySettings[ 0 ] || - ( minDate && printDate < minDate ) || ( maxDate && printDate > maxDate ); - tbody += "<td class='" + - ( ( dow + firstDay + 6 ) % 7 >= 5 ? " ui-datepicker-week-end" : "" ) + // highlight weekends - ( otherMonth ? " ui-datepicker-other-month" : "" ) + // highlight days from other months - ( ( printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent ) || // user pressed key - ( defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime() ) ? + _setOption: function( key, value ) { + this._super( key, value ); - // or defaultDate is current printedDate and defaultDate is selectedDate - " " + this._dayOverClass : "" ) + // highlight selected day - ( unselectable ? " " + this._unselectableClass + " ui-state-disabled" : "" ) + // highlight unselectable days - ( otherMonth && !showOtherMonths ? "" : " " + daySettings[ 1 ] + // highlight custom dates - ( printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "" ) + // highlight selected day - ( printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "" ) ) + "'" + // highlight today (if different) - ( ( !otherMonth || showOtherMonths ) && daySettings[ 2 ] ? " title='" + daySettings[ 2 ].replace( /'/g, "'" ) + "'" : "" ) + // cell title - ( unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'" ) + ">" + // actions - ( otherMonth && !showOtherMonths ? " " : // display for other months - ( unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + - ( printDate.getTime() === today.getTime() ? " ui-state-highlight" : "" ) + - ( printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "" ) + // highlight selected day - ( otherMonth ? " ui-priority-secondary" : "" ) + // distinguish dates from other months - "' href='#' aria-current='" + ( printDate.getTime() === currentDate.getTime() ? "true" : "false" ) + // mark date as selected for screen reader - "' data-date='" + printDate.getDate() + // store date as data - "'>" + printDate.getDate() + "</a>" ) ) + "</td>"; // display selectable date - printDate.setDate( printDate.getDate() + 1 ); - printDate = this._daylightSavingAdjust( printDate ); - } - calender += tbody + "</tr>"; - } - drawMonth++; - if ( drawMonth > 11 ) { - drawMonth = 0; - drawYear++; - } - calender += "</tbody></table>" + ( isMultiMonth ? "</div>" + - ( ( numMonths[ 0 ] > 0 && col === numMonths[ 1 ] - 1 ) ? "<div class='ui-datepicker-row-break'></div>" : "" ) : "" ); - group += calender; + switch ( key ) { + case "handles": + this._removeHandles(); + this._setupHandles(); + break; + case "aspectRatio": + this._aspectRatio = !!value; + break; + default: + break; + } + }, + + _setupHandles: function() { + var o = this.options, handle, i, n, hname, axis, that = this; + this.handles = o.handles || + ( !$( ".ui-resizable-handle", this.element ).length ? + "e,s,se" : { + n: ".ui-resizable-n", + e: ".ui-resizable-e", + s: ".ui-resizable-s", + w: ".ui-resizable-w", + se: ".ui-resizable-se", + sw: ".ui-resizable-sw", + ne: ".ui-resizable-ne", + nw: ".ui-resizable-nw" + } ); + + this._handles = $(); + this._addedHandles = $(); + if ( this.handles.constructor === String ) { + + if ( this.handles === "all" ) { + this.handles = "n,e,s,w,se,sw,ne,nw"; } - html += group; - } - html += buttonPanel; - inst._keyEvent = false; - return html; - }, - /* Generate the month and year header. */ - _generateMonthYearHeader: function( inst, drawMonth, drawYear, minDate, maxDate, - secondary, monthNames, monthNamesShort ) { + n = this.handles.split( "," ); + this.handles = {}; - var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, - changeMonth = this._get( inst, "changeMonth" ), - changeYear = this._get( inst, "changeYear" ), - showMonthAfterYear = this._get( inst, "showMonthAfterYear" ), - selectMonthLabel = this._get( inst, "selectMonthLabel" ), - selectYearLabel = this._get( inst, "selectYearLabel" ), - html = "<div class='ui-datepicker-title'>", - monthHtml = ""; + for ( i = 0; i < n.length; i++ ) { - // Month selection - if ( secondary || !changeMonth ) { - monthHtml += "<span class='ui-datepicker-month'>" + monthNames[ drawMonth ] + "</span>"; - } else { - inMinYear = ( minDate && minDate.getFullYear() === drawYear ); - inMaxYear = ( maxDate && maxDate.getFullYear() === drawYear ); - monthHtml += "<select class='ui-datepicker-month' aria-label='" + selectMonthLabel + "' data-handler='selectMonth' data-event='change'>"; - for ( month = 0; month < 12; month++ ) { - if ( ( !inMinYear || month >= minDate.getMonth() ) && ( !inMaxYear || month <= maxDate.getMonth() ) ) { - monthHtml += "<option value='" + month + "'" + - ( month === drawMonth ? " selected='selected'" : "" ) + - ">" + monthNamesShort[ month ] + "</option>"; + handle = String.prototype.trim.call( n[ i ] ); + hname = "ui-resizable-" + handle; + axis = $( "<div>" ); + this._addClass( axis, "ui-resizable-handle " + hname ); + + axis.css( { zIndex: o.zIndex } ); + + this.handles[ handle ] = ".ui-resizable-" + handle; + if ( !this.element.children( this.handles[ handle ] ).length ) { + this.element.append( axis ); + this._addedHandles = this._addedHandles.add( axis ); } } - monthHtml += "</select>"; - } - if ( !showMonthAfterYear ) { - html += monthHtml + ( secondary || !( changeMonth && changeYear ) ? " " : "" ); } - // Year selection - if ( !inst.yearshtml ) { - inst.yearshtml = ""; - if ( secondary || !changeYear ) { - html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; - } else { + this._renderAxis = function( target ) { - // determine range of years to display - years = this._get( inst, "yearRange" ).split( ":" ); - thisYear = new Date().getFullYear(); - determineYear = function( value ) { - var year = ( value.match( /c[+\-].*/ ) ? drawYear + parseInt( value.substring( 1 ), 10 ) : - ( value.match( /[+\-].*/ ) ? thisYear + parseInt( value, 10 ) : - parseInt( value, 10 ) ) ); - return ( isNaN( year ) ? thisYear : year ); - }; - year = determineYear( years[ 0 ] ); - endYear = Math.max( year, determineYear( years[ 1 ] || "" ) ); - year = ( minDate ? Math.max( year, minDate.getFullYear() ) : year ); - endYear = ( maxDate ? Math.min( endYear, maxDate.getFullYear() ) : endYear ); - inst.yearshtml += "<select class='ui-datepicker-year' aria-label='" + selectYearLabel + "' data-handler='selectYear' data-event='change'>"; - for ( ; year <= endYear; year++ ) { - inst.yearshtml += "<option value='" + year + "'" + - ( year === drawYear ? " selected='selected'" : "" ) + - ">" + year + "</option>"; + var i, axis, padPos, padWrapper; + + target = target || this.element; + + for ( i in this.handles ) { + + if ( this.handles[ i ].constructor === String ) { + this.handles[ i ] = this.element.children( this.handles[ i ] ).first().show(); + } else if ( this.handles[ i ].jquery || this.handles[ i ].nodeType ) { + this.handles[ i ] = $( this.handles[ i ] ); + this._on( this.handles[ i ], { "mousedown": that._mouseDown } ); } - inst.yearshtml += "</select>"; - html += inst.yearshtml; - inst.yearshtml = null; + if ( this.elementIsWrapper && + this.originalElement[ 0 ] + .nodeName + .match( /^(textarea|input|select|button)$/i ) ) { + axis = $( this.handles[ i ], this.element ); + + padWrapper = /sw|ne|nw|se|n|s/.test( i ) ? + axis.outerHeight() : + axis.outerWidth(); + + padPos = [ "padding", + /ne|nw|n/.test( i ) ? "Top" : + /se|sw|s/.test( i ) ? "Bottom" : + /^e$/.test( i ) ? "Right" : "Left" ].join( "" ); + + target.css( padPos, padWrapper ); + + this._proportionallyResize(); + } + + this._handles = this._handles.add( this.handles[ i ] ); } - } + }; - html += this._get( inst, "yearSuffix" ); - if ( showMonthAfterYear ) { - html += ( secondary || !( changeMonth && changeYear ) ? " " : "" ) + monthHtml; - } - html += "</div>"; // Close datepicker_header - return html; - }, + // TODO: make renderAxis a prototype function + this._renderAxis( this.element ); - /* Adjust one of the date sub-fields. */ - _adjustInstDate: function( inst, offset, period ) { - var year = inst.selectedYear + ( period === "Y" ? offset : 0 ), - month = inst.selectedMonth + ( period === "M" ? offset : 0 ), - day = Math.min( inst.selectedDay, this._getDaysInMonth( year, month ) ) + ( period === "D" ? offset : 0 ), - date = this._restrictMinMax( inst, this._daylightSavingAdjust( new Date( year, month, day ) ) ); + this._handles = this._handles.add( this.element.find( ".ui-resizable-handle" ) ); + this._handles.disableSelection(); - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); - if ( period === "M" || period === "Y" ) { - this._notifyChange( inst ); + this._handles.on( "mouseover", function() { + if ( !that.resizing ) { + if ( this.className ) { + axis = this.className.match( /ui-resizable-(se|sw|ne|nw|n|e|s|w)/i ); + } + that.axis = axis && axis[ 1 ] ? axis[ 1 ] : "se"; + } + } ); + + if ( o.autoHide ) { + this._handles.hide(); + this._addClass( "ui-resizable-autohide" ); } }, - /* Ensure a date is within any min/max bounds. */ - _restrictMinMax: function( inst, date ) { - var minDate = this._getMinMaxDate( inst, "min" ), - maxDate = this._getMinMaxDate( inst, "max" ), - newDate = ( minDate && date < minDate ? minDate : date ); - return ( maxDate && newDate > maxDate ? maxDate : newDate ); + _removeHandles: function() { + this._addedHandles.remove(); }, - /* Notify change of month/year. */ - _notifyChange: function( inst ) { - var onChange = this._get( inst, "onChangeMonthYear" ); - if ( onChange ) { - onChange.apply( ( inst.input ? inst.input[ 0 ] : null ), - [ inst.selectedYear, inst.selectedMonth + 1, inst ] ); + _mouseCapture: function( event ) { + var i, handle, + capture = false; + + for ( i in this.handles ) { + handle = $( this.handles[ i ] )[ 0 ]; + if ( handle === event.target || $.contains( handle, event.target ) ) { + capture = true; + } } - }, - /* Determine the number of months to show. */ - _getNumberOfMonths: function( inst ) { - var numMonths = this._get( inst, "numberOfMonths" ); - return ( numMonths == null ? [ 1, 1 ] : ( typeof numMonths === "number" ? [ 1, numMonths ] : numMonths ) ); + return !this.options.disabled && capture; }, - /* Determine the current maximum date - ensure no time components are set. */ - _getMinMaxDate: function( inst, minMax ) { - return this._determineDate( inst, this._get( inst, minMax + "Date" ), null ); - }, + _mouseStart: function( event ) { - /* Find the number of days in a given month. */ - _getDaysInMonth: function( year, month ) { - return 32 - this._daylightSavingAdjust( new Date( year, month, 32 ) ).getDate(); - }, + var curleft, curtop, cursor, + o = this.options, + el = this.element; - /* Find the day of the week of the first of a month. */ - _getFirstDayOfMonth: function( year, month ) { - return new Date( year, month, 1 ).getDay(); - }, + this.resizing = true; - /* Determines if we should allow a "next/prev" month display change. */ - _canAdjustMonth: function( inst, offset, curYear, curMonth ) { - var numMonths = this._getNumberOfMonths( inst ), - date = this._daylightSavingAdjust( new Date( curYear, - curMonth + ( offset < 0 ? offset : numMonths[ 0 ] * numMonths[ 1 ] ), 1 ) ); + this._renderProxy(); - if ( offset < 0 ) { - date.setDate( this._getDaysInMonth( date.getFullYear(), date.getMonth() ) ); + curleft = this._num( this.helper.css( "left" ) ); + curtop = this._num( this.helper.css( "top" ) ); + + if ( o.containment ) { + curleft += $( o.containment ).scrollLeft() || 0; + curtop += $( o.containment ).scrollTop() || 0; } - return this._isInRange( inst, date ); - }, - /* Is the given date in the accepted range? */ - _isInRange: function( inst, date ) { - var yearSplit, currentYear, - minDate = this._getMinMaxDate( inst, "min" ), - maxDate = this._getMinMaxDate( inst, "max" ), - minYear = null, - maxYear = null, - years = this._get( inst, "yearRange" ); - if ( years ) { - yearSplit = years.split( ":" ); - currentYear = new Date().getFullYear(); - minYear = parseInt( yearSplit[ 0 ], 10 ); - maxYear = parseInt( yearSplit[ 1 ], 10 ); - if ( yearSplit[ 0 ].match( /[+\-].*/ ) ) { - minYear += currentYear; - } - if ( yearSplit[ 1 ].match( /[+\-].*/ ) ) { - maxYear += currentYear; - } - } + this.offset = this.helper.offset(); + this.position = { left: curleft, top: curtop }; + + this.size = this._helper ? { + width: this.helper.width(), + height: this.helper.height() + } : { + width: el.width(), + height: el.height() + }; + + this.originalSize = this._helper ? { + width: el.outerWidth(), + height: el.outerHeight() + } : { + width: el.width(), + height: el.height() + }; + + this.sizeDiff = { + width: el.outerWidth() - el.width(), + height: el.outerHeight() - el.height() + }; + + this.originalPosition = { left: curleft, top: curtop }; + this.originalMousePosition = { left: event.pageX, top: event.pageY }; + + this.aspectRatio = ( typeof o.aspectRatio === "number" ) ? + o.aspectRatio : + ( ( this.originalSize.width / this.originalSize.height ) || 1 ); - return ( ( !minDate || date.getTime() >= minDate.getTime() ) && - ( !maxDate || date.getTime() <= maxDate.getTime() ) && - ( !minYear || date.getFullYear() >= minYear ) && - ( !maxYear || date.getFullYear() <= maxYear ) ); - }, + cursor = $( ".ui-resizable-" + this.axis ).css( "cursor" ); + $( "body" ).css( "cursor", cursor === "auto" ? this.axis + "-resize" : cursor ); - /* Provide the configuration settings for formatting/parsing. */ - _getFormatConfig: function( inst ) { - var shortYearCutoff = this._get( inst, "shortYearCutoff" ); - shortYearCutoff = ( typeof shortYearCutoff !== "string" ? shortYearCutoff : - new Date().getFullYear() % 100 + parseInt( shortYearCutoff, 10 ) ); - return { shortYearCutoff: shortYearCutoff, - dayNamesShort: this._get( inst, "dayNamesShort" ), dayNames: this._get( inst, "dayNames" ), - monthNamesShort: this._get( inst, "monthNamesShort" ), monthNames: this._get( inst, "monthNames" ) }; + this._addClass( "ui-resizable-resizing" ); + this._propagate( "start", event ); + return true; }, - /* Format the given date for display. */ - _formatDate: function( inst, day, month, year ) { - if ( !day ) { - inst.currentDay = inst.selectedDay; - inst.currentMonth = inst.selectedMonth; - inst.currentYear = inst.selectedYear; - } - var date = ( day ? ( typeof day === "object" ? day : - this._daylightSavingAdjust( new Date( year, month, day ) ) ) : - this._daylightSavingAdjust( new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); - return this.formatDate( this._get( inst, "dateFormat" ), date, this._getFormatConfig( inst ) ); - } -} ); + _mouseDrag: function( event ) { -/* - * Bind hover events for datepicker elements. - * Done via delegate so the binding only occurs once in the lifetime of the parent div. - * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. - */ -function datepicker_bindHover( dpDiv ) { - var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; - return dpDiv.on( "mouseout", selector, function() { - $( this ).removeClass( "ui-state-hover" ); - if ( this.className.indexOf( "ui-datepicker-prev" ) !== -1 ) { - $( this ).removeClass( "ui-datepicker-prev-hover" ); - } - if ( this.className.indexOf( "ui-datepicker-next" ) !== -1 ) { - $( this ).removeClass( "ui-datepicker-next-hover" ); - } - } ) - .on( "mouseover", selector, datepicker_handleMouseover ); -} + var data, props, + smp = this.originalMousePosition, + a = this.axis, + dx = ( event.pageX - smp.left ) || 0, + dy = ( event.pageY - smp.top ) || 0, + trigger = this._change[ a ]; -function datepicker_handleMouseover() { - if ( !$.datepicker._isDisabledDatepicker( datepicker_instActive.inline ? datepicker_instActive.dpDiv.parent()[ 0 ] : datepicker_instActive.input[ 0 ] ) ) { - $( this ).parents( ".ui-datepicker-calendar" ).find( "a" ).removeClass( "ui-state-hover" ); - $( this ).addClass( "ui-state-hover" ); - if ( this.className.indexOf( "ui-datepicker-prev" ) !== -1 ) { - $( this ).addClass( "ui-datepicker-prev-hover" ); - } - if ( this.className.indexOf( "ui-datepicker-next" ) !== -1 ) { - $( this ).addClass( "ui-datepicker-next-hover" ); + this._updatePrevProperties(); + + if ( !trigger ) { + return false; } - } -} -/* jQuery extend now ignores nulls! */ -function datepicker_extendRemove( target, props ) { - $.extend( target, props ); - for ( var name in props ) { - if ( props[ name ] == null ) { - target[ name ] = props[ name ]; + data = trigger.apply( this, [ event, dx, dy ] ); + + this._updateVirtualBoundaries( event.shiftKey ); + if ( this._aspectRatio || event.shiftKey ) { + data = this._updateRatio( data, event ); } - } - return target; -} -/* Invoke the datepicker functionality. - @param options string - a command, optionally followed by additional parameters or - Object - settings for attaching new datepicker functionality - @return jQuery object */ -$.fn.datepicker = function( options ) { + data = this._respectSize( data, event ); - /* Verify an empty collection wasn't passed - Fixes #6976 */ - if ( !this.length ) { - return this; - } + this._updateCache( data ); - /* Initialise the date picker. */ - if ( !$.datepicker.initialized ) { - $( document ).on( "mousedown", $.datepicker._checkExternalClick ); - $.datepicker.initialized = true; - } + this._propagate( "resize", event ); - /* Append datepicker main container to body if not exist. */ - if ( $( "#" + $.datepicker._mainDivId ).length === 0 ) { - $( "body" ).append( $.datepicker.dpDiv ); - } + props = this._applyChanges(); - var otherArgs = Array.prototype.slice.call( arguments, 1 ); - if ( typeof options === "string" && ( options === "isDisabled" || options === "getDate" || options === "widget" ) ) { - return $.datepicker[ "_" + options + "Datepicker" ]. - apply( $.datepicker, [ this[ 0 ] ].concat( otherArgs ) ); - } - if ( options === "option" && arguments.length === 2 && typeof arguments[ 1 ] === "string" ) { - return $.datepicker[ "_" + options + "Datepicker" ]. - apply( $.datepicker, [ this[ 0 ] ].concat( otherArgs ) ); - } - return this.each( function() { - if ( typeof options === "string" ) { - $.datepicker[ "_" + options + "Datepicker" ] - .apply( $.datepicker, [ this ].concat( otherArgs ) ); - } else { - $.datepicker._attachDatepicker( this, options ); + if ( !this._helper && this._proportionallyResizeElements.length ) { + this._proportionallyResize(); } - } ); -}; -$.datepicker = new Datepicker(); // singleton instance -$.datepicker.initialized = false; -$.datepicker.uuid = new Date().getTime(); -$.datepicker.version = "1.13.1"; + if ( !$.isEmptyObject( props ) ) { + this._updatePrevProperties(); + this._trigger( "resize", event, this.ui() ); + this._applyChanges(); + } -var widgetsDatepicker = $.datepicker; + return false; + }, + _mouseStop: function( event ) { -/*! - * jQuery UI Dialog 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this.resizing = false; + var pr, ista, soffseth, soffsetw, s, left, top, + o = this.options, that = this; -//>>label: Dialog -//>>group: Widgets -//>>description: Displays customizable dialog windows. -//>>docs: http://api.jqueryui.com/dialog/ -//>>demos: http://jqueryui.com/dialog/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/dialog.css -//>>css.theme: ../../themes/base/theme.css + if ( this._helper ) { + pr = this._proportionallyResizeElements; + ista = pr.length && ( /textarea/i ).test( pr[ 0 ].nodeName ); + soffseth = ista && this._hasScroll( pr[ 0 ], "left" ) ? 0 : that.sizeDiff.height; + soffsetw = ista ? 0 : that.sizeDiff.width; -$.widget( "ui.dialog", { - version: "1.13.1", - options: { - appendTo: "body", - autoOpen: true, - buttons: [], - classes: { - "ui-dialog": "ui-corner-all", - "ui-dialog-titlebar": "ui-corner-all" - }, - closeOnEscape: true, - closeText: "Close", - draggable: true, - hide: null, - height: "auto", - maxHeight: null, - maxWidth: null, - minHeight: 150, - minWidth: 150, - modal: false, - position: { - my: "center", - at: "center", - of: window, - collision: "fit", + s = { + width: ( that.helper.width() - soffsetw ), + height: ( that.helper.height() - soffseth ) + }; + left = ( parseFloat( that.element.css( "left" ) ) + + ( that.position.left - that.originalPosition.left ) ) || null; + top = ( parseFloat( that.element.css( "top" ) ) + + ( that.position.top - that.originalPosition.top ) ) || null; - // Ensure the titlebar is always visible - using: function( pos ) { - var topOffset = $( this ).css( pos ).offset().top; - if ( topOffset < 0 ) { - $( this ).css( "top", pos.top - topOffset ); - } + if ( !o.animate ) { + this.element.css( $.extend( s, { top: top, left: left } ) ); } - }, - resizable: true, - show: null, - title: null, - width: 300, - - // Callbacks - beforeClose: null, - close: null, - drag: null, - dragStart: null, - dragStop: null, - focus: null, - open: null, - resize: null, - resizeStart: null, - resizeStop: null - }, - - sizeRelatedOptions: { - buttons: true, - height: true, - maxHeight: true, - maxWidth: true, - minHeight: true, - minWidth: true, - width: true - }, - resizableRelatedOptions: { - maxHeight: true, - maxWidth: true, - minHeight: true, - minWidth: true - }, + that.helper.height( that.size.height ); + that.helper.width( that.size.width ); - _create: function() { - this.originalCss = { - display: this.element[ 0 ].style.display, - width: this.element[ 0 ].style.width, - minHeight: this.element[ 0 ].style.minHeight, - maxHeight: this.element[ 0 ].style.maxHeight, - height: this.element[ 0 ].style.height - }; - this.originalPosition = { - parent: this.element.parent(), - index: this.element.parent().children().index( this.element ) - }; - this.originalTitle = this.element.attr( "title" ); - if ( this.options.title == null && this.originalTitle != null ) { - this.options.title = this.originalTitle; + if ( this._helper && !o.animate ) { + this._proportionallyResize(); + } } - // Dialogs can't be disabled - if ( this.options.disabled ) { - this.options.disabled = false; + $( "body" ).css( "cursor", "auto" ); + + this._removeClass( "ui-resizable-resizing" ); + + this._propagate( "stop", event ); + + if ( this._helper ) { + this.helper.remove(); } - this._createWrapper(); + return false; - this.element - .show() - .removeAttr( "title" ) - .appendTo( this.uiDialog ); + }, - this._addClass( "ui-dialog-content", "ui-widget-content" ); + _updatePrevProperties: function() { + this.prevPosition = { + top: this.position.top, + left: this.position.left + }; + this.prevSize = { + width: this.size.width, + height: this.size.height + }; + }, - this._createTitlebar(); - this._createButtonPane(); + _applyChanges: function() { + var props = {}; - if ( this.options.draggable && $.fn.draggable ) { - this._makeDraggable(); + if ( this.position.top !== this.prevPosition.top ) { + props.top = this.position.top + "px"; } - if ( this.options.resizable && $.fn.resizable ) { - this._makeResizable(); + if ( this.position.left !== this.prevPosition.left ) { + props.left = this.position.left + "px"; + } + if ( this.size.width !== this.prevSize.width ) { + props.width = this.size.width + "px"; + } + if ( this.size.height !== this.prevSize.height ) { + props.height = this.size.height + "px"; } - this._isOpen = false; + this.helper.css( props ); - this._trackFocus(); + return props; }, - _init: function() { - if ( this.options.autoOpen ) { - this.open(); + _updateVirtualBoundaries: function( forceAspectRatio ) { + var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b, + o = this.options; + + b = { + minWidth: this._isNumber( o.minWidth ) ? o.minWidth : 0, + maxWidth: this._isNumber( o.maxWidth ) ? o.maxWidth : Infinity, + minHeight: this._isNumber( o.minHeight ) ? o.minHeight : 0, + maxHeight: this._isNumber( o.maxHeight ) ? o.maxHeight : Infinity + }; + + if ( this._aspectRatio || forceAspectRatio ) { + pMinWidth = b.minHeight * this.aspectRatio; + pMinHeight = b.minWidth / this.aspectRatio; + pMaxWidth = b.maxHeight * this.aspectRatio; + pMaxHeight = b.maxWidth / this.aspectRatio; + + if ( pMinWidth > b.minWidth ) { + b.minWidth = pMinWidth; + } + if ( pMinHeight > b.minHeight ) { + b.minHeight = pMinHeight; + } + if ( pMaxWidth < b.maxWidth ) { + b.maxWidth = pMaxWidth; + } + if ( pMaxHeight < b.maxHeight ) { + b.maxHeight = pMaxHeight; + } } + this._vBoundaries = b; }, - _appendTo: function() { - var element = this.options.appendTo; - if ( element && ( element.jquery || element.nodeType ) ) { - return $( element ); + _updateCache: function( data ) { + this.offset = this.helper.offset(); + if ( this._isNumber( data.left ) ) { + this.position.left = data.left; + } + if ( this._isNumber( data.top ) ) { + this.position.top = data.top; + } + if ( this._isNumber( data.height ) ) { + this.size.height = data.height; + } + if ( this._isNumber( data.width ) ) { + this.size.width = data.width; } - return this.document.find( element || "body" ).eq( 0 ); }, - _destroy: function() { - var next, - originalPosition = this.originalPosition; + _updateRatio: function( data ) { - this._untrackInstance(); - this._destroyOverlay(); + var cpos = this.position, + csize = this.size, + a = this.axis; - this.element - .removeUniqueId() - .css( this.originalCss ) + if ( this._isNumber( data.height ) ) { + data.width = ( data.height * this.aspectRatio ); + } else if ( this._isNumber( data.width ) ) { + data.height = ( data.width / this.aspectRatio ); + } - // Without detaching first, the following becomes really slow - .detach(); + if ( a === "sw" ) { + data.left = cpos.left + ( csize.width - data.width ); + data.top = null; + } + if ( a === "nw" ) { + data.top = cpos.top + ( csize.height - data.height ); + data.left = cpos.left + ( csize.width - data.width ); + } - this.uiDialog.remove(); + return data; + }, - if ( this.originalTitle ) { - this.element.attr( "title", this.originalTitle ); + _respectSize: function( data ) { + + var o = this._vBoundaries, + a = this.axis, + ismaxw = this._isNumber( data.width ) && o.maxWidth && ( o.maxWidth < data.width ), + ismaxh = this._isNumber( data.height ) && o.maxHeight && ( o.maxHeight < data.height ), + isminw = this._isNumber( data.width ) && o.minWidth && ( o.minWidth > data.width ), + isminh = this._isNumber( data.height ) && o.minHeight && ( o.minHeight > data.height ), + dw = this.originalPosition.left + this.originalSize.width, + dh = this.originalPosition.top + this.originalSize.height, + cw = /sw|nw|w/.test( a ), ch = /nw|ne|n/.test( a ); + if ( isminw ) { + data.width = o.minWidth; + } + if ( isminh ) { + data.height = o.minHeight; + } + if ( ismaxw ) { + data.width = o.maxWidth; + } + if ( ismaxh ) { + data.height = o.maxHeight; } - next = originalPosition.parent.children().eq( originalPosition.index ); + if ( isminw && cw ) { + data.left = dw - o.minWidth; + } + if ( ismaxw && cw ) { + data.left = dw - o.maxWidth; + } + if ( isminh && ch ) { + data.top = dh - o.minHeight; + } + if ( ismaxh && ch ) { + data.top = dh - o.maxHeight; + } - // Don't try to place the dialog next to itself (#8613) - if ( next.length && next[ 0 ] !== this.element[ 0 ] ) { - next.before( this.element ); - } else { - originalPosition.parent.append( this.element ); + // Fixing jump error on top/left - bug #2330 + if ( !data.width && !data.height && !data.left && data.top ) { + data.top = null; + } else if ( !data.width && !data.height && !data.top && data.left ) { + data.left = null; } - }, - widget: function() { - return this.uiDialog; + return data; }, - disable: $.noop, - enable: $.noop, - - close: function( event ) { - var that = this; + _getPaddingPlusBorderDimensions: function( element ) { + var i = 0, + widths = [], + borders = [ + element.css( "borderTopWidth" ), + element.css( "borderRightWidth" ), + element.css( "borderBottomWidth" ), + element.css( "borderLeftWidth" ) + ], + paddings = [ + element.css( "paddingTop" ), + element.css( "paddingRight" ), + element.css( "paddingBottom" ), + element.css( "paddingLeft" ) + ]; - if ( !this._isOpen || this._trigger( "beforeClose", event ) === false ) { - return; + for ( ; i < 4; i++ ) { + widths[ i ] = ( parseFloat( borders[ i ] ) || 0 ); + widths[ i ] += ( parseFloat( paddings[ i ] ) || 0 ); } - this._isOpen = false; - this._focusedElement = null; - this._destroyOverlay(); - this._untrackInstance(); + return { + height: widths[ 0 ] + widths[ 2 ], + width: widths[ 1 ] + widths[ 3 ] + }; + }, - if ( !this.opener.filter( ":focusable" ).trigger( "focus" ).length ) { + _proportionallyResize: function() { - // Hiding a focused element doesn't trigger blur in WebKit - // so in case we have nothing to focus on, explicitly blur the active element - // https://bugs.webkit.org/show_bug.cgi?id=47182 - $.ui.safeBlur( $.ui.safeActiveElement( this.document[ 0 ] ) ); + if ( !this._proportionallyResizeElements.length ) { + return; } - this._hide( this.uiDialog, this.options.hide, function() { - that._trigger( "close", event ); - } ); - }, + var prel, + i = 0, + element = this.helper || this.element; - isOpen: function() { - return this._isOpen; - }, + for ( ; i < this._proportionallyResizeElements.length; i++ ) { - moveToTop: function() { - this._moveToTop(); - }, + prel = this._proportionallyResizeElements[ i ]; - _moveToTop: function( event, silent ) { - var moved = false, - zIndices = this.uiDialog.siblings( ".ui-front:visible" ).map( function() { - return +$( this ).css( "z-index" ); - } ).get(), - zIndexMax = Math.max.apply( null, zIndices ); + // TODO: Seems like a bug to cache this.outerDimensions + // considering that we are in a loop. + if ( !this.outerDimensions ) { + this.outerDimensions = this._getPaddingPlusBorderDimensions( prel ); + } + + prel.css( { + height: ( element.height() - this.outerDimensions.height ) || 0, + width: ( element.width() - this.outerDimensions.width ) || 0 + } ); - if ( zIndexMax >= +this.uiDialog.css( "z-index" ) ) { - this.uiDialog.css( "z-index", zIndexMax + 1 ); - moved = true; } - if ( moved && !silent ) { - this._trigger( "focus", event ); - } - return moved; }, - open: function() { - var that = this; - if ( this._isOpen ) { - if ( this._moveToTop() ) { - this._focusTabbable(); - } - return; - } + _renderProxy: function() { - this._isOpen = true; - this.opener = $( $.ui.safeActiveElement( this.document[ 0 ] ) ); + var el = this.element, o = this.options; + this.elementOffset = el.offset(); - this._size(); - this._position(); - this._createOverlay(); - this._moveToTop( null, true ); + if ( this._helper ) { - // Ensure the overlay is moved to the top with the dialog, but only when - // opening. The overlay shouldn't move after the dialog is open so that - // modeless dialogs opened after the modal dialog stack properly. - if ( this.overlay ) { - this.overlay.css( "z-index", this.uiDialog.css( "z-index" ) - 1 ); - } + this.helper = this.helper || $( "<div></div>" ).css( { overflow: "hidden" } ); - this._show( this.uiDialog, this.options.show, function() { - that._focusTabbable(); - that._trigger( "focus" ); - } ); + this._addClass( this.helper, this._helper ); + this.helper.css( { + width: this.element.outerWidth(), + height: this.element.outerHeight(), + position: "absolute", + left: this.elementOffset.left + "px", + top: this.elementOffset.top + "px", + zIndex: ++o.zIndex //TODO: Don't modify option + } ); - // Track the dialog immediately upon opening in case a focus event - // somehow occurs outside of the dialog before an element inside the - // dialog is focused (#10152) - this._makeFocusTarget(); + this.helper + .appendTo( "body" ) + .disableSelection(); - this._trigger( "open" ); - }, + } else { + this.helper = this.element; + } - _focusTabbable: function() { + }, - // Set focus to the first match: - // 1. An element that was focused previously - // 2. First element inside the dialog matching [autofocus] - // 3. Tabbable element inside the content element - // 4. Tabbable element inside the buttonpane - // 5. The close button - // 6. The dialog itself - var hasFocus = this._focusedElement; - if ( !hasFocus ) { - hasFocus = this.element.find( "[autofocus]" ); - } - if ( !hasFocus.length ) { - hasFocus = this.element.find( ":tabbable" ); - } - if ( !hasFocus.length ) { - hasFocus = this.uiDialogButtonPane.find( ":tabbable" ); - } - if ( !hasFocus.length ) { - hasFocus = this.uiDialogTitlebarClose.filter( ":tabbable" ); - } - if ( !hasFocus.length ) { - hasFocus = this.uiDialog; + _change: { + e: function( event, dx ) { + return { width: this.originalSize.width + dx }; + }, + w: function( event, dx ) { + var cs = this.originalSize, sp = this.originalPosition; + return { left: sp.left + dx, width: cs.width - dx }; + }, + n: function( event, dx, dy ) { + var cs = this.originalSize, sp = this.originalPosition; + return { top: sp.top + dy, height: cs.height - dy }; + }, + s: function( event, dx, dy ) { + return { height: this.originalSize.height + dy }; + }, + se: function( event, dx, dy ) { + return $.extend( this._change.s.apply( this, arguments ), + this._change.e.apply( this, [ event, dx, dy ] ) ); + }, + sw: function( event, dx, dy ) { + return $.extend( this._change.s.apply( this, arguments ), + this._change.w.apply( this, [ event, dx, dy ] ) ); + }, + ne: function( event, dx, dy ) { + return $.extend( this._change.n.apply( this, arguments ), + this._change.e.apply( this, [ event, dx, dy ] ) ); + }, + nw: function( event, dx, dy ) { + return $.extend( this._change.n.apply( this, arguments ), + this._change.w.apply( this, [ event, dx, dy ] ) ); } - hasFocus.eq( 0 ).trigger( "focus" ); }, - _restoreTabbableFocus: function() { - var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ), - isActive = this.uiDialog[ 0 ] === activeElement || - $.contains( this.uiDialog[ 0 ], activeElement ); - if ( !isActive ) { - this._focusTabbable(); + _propagate: function( n, event ) { + $.ui.plugin.call( this, n, [ event, this.ui() ] ); + if ( n !== "resize" ) { + this._trigger( n, event, this.ui() ); } }, - _keepFocus: function( event ) { - event.preventDefault(); - this._restoreTabbableFocus(); - - // support: IE - // IE <= 8 doesn't prevent moving focus even with event.preventDefault() - // so we check again later - this._delay( this._restoreTabbableFocus ); - }, + plugins: {}, - _createWrapper: function() { - this.uiDialog = $( "<div>" ) - .hide() - .attr( { + ui: function() { + return { + originalElement: this.originalElement, + element: this.element, + helper: this.helper, + position: this.position, + size: this.size, + originalSize: this.originalSize, + originalPosition: this.originalPosition + }; + } - // Setting tabIndex makes the div focusable - tabIndex: -1, - role: "dialog" - } ) - .appendTo( this._appendTo() ); +} ); - this._addClass( this.uiDialog, "ui-dialog", "ui-widget ui-widget-content ui-front" ); - this._on( this.uiDialog, { - keydown: function( event ) { - if ( this.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && - event.keyCode === $.ui.keyCode.ESCAPE ) { - event.preventDefault(); - this.close( event ); - return; - } +/* + * Resizable Extensions + */ - // Prevent tabbing out of dialogs - if ( event.keyCode !== $.ui.keyCode.TAB || event.isDefaultPrevented() ) { - return; - } - var tabbables = this.uiDialog.find( ":tabbable" ), - first = tabbables.first(), - last = tabbables.last(); +$.ui.plugin.add( "resizable", "animate", { - if ( ( event.target === last[ 0 ] || event.target === this.uiDialog[ 0 ] ) && - !event.shiftKey ) { - this._delay( function() { - first.trigger( "focus" ); - } ); - event.preventDefault(); - } else if ( ( event.target === first[ 0 ] || - event.target === this.uiDialog[ 0 ] ) && event.shiftKey ) { - this._delay( function() { - last.trigger( "focus" ); - } ); - event.preventDefault(); - } + stop: function( event ) { + var that = $( this ).resizable( "instance" ), + o = that.options, + pr = that._proportionallyResizeElements, + ista = pr.length && ( /textarea/i ).test( pr[ 0 ].nodeName ), + soffseth = ista && that._hasScroll( pr[ 0 ], "left" ) ? 0 : that.sizeDiff.height, + soffsetw = ista ? 0 : that.sizeDiff.width, + style = { + width: ( that.size.width - soffsetw ), + height: ( that.size.height - soffseth ) }, - mousedown: function( event ) { - if ( this._moveToTop( event ) ) { - this._focusTabbable(); + left = ( parseFloat( that.element.css( "left" ) ) + + ( that.position.left - that.originalPosition.left ) ) || null, + top = ( parseFloat( that.element.css( "top" ) ) + + ( that.position.top - that.originalPosition.top ) ) || null; + + that.element.animate( + $.extend( style, top && left ? { top: top, left: left } : {} ), { + duration: o.animateDuration, + easing: o.animateEasing, + step: function() { + + var data = { + width: parseFloat( that.element.css( "width" ) ), + height: parseFloat( that.element.css( "height" ) ), + top: parseFloat( that.element.css( "top" ) ), + left: parseFloat( that.element.css( "left" ) ) + }; + + if ( pr && pr.length ) { + $( pr[ 0 ] ).css( { width: data.width, height: data.height } ); + } + + // Propagating resize, and updating values for each animation step + that._updateCache( data ); + that._propagate( "resize", event ); + } } - } ); + ); + } + +} ); + +$.ui.plugin.add( "resizable", "containment", { + + start: function() { + var element, p, co, ch, cw, width, height, + that = $( this ).resizable( "instance" ), + o = that.options, + el = that.element, + oc = o.containment, + ce = ( oc instanceof $ ) ? + oc.get( 0 ) : + ( /parent/.test( oc ) ) ? el.parent().get( 0 ) : oc; + + if ( !ce ) { + return; + } + + that.containerElement = $( ce ); + + if ( /document/.test( oc ) || oc === document ) { + that.containerOffset = { + left: 0, + top: 0 + }; + that.containerPosition = { + left: 0, + top: 0 + }; - // We assume that any existing aria-describedby attribute means - // that the dialog content is marked up properly - // otherwise we brute force the content as the description - if ( !this.element.find( "[aria-describedby]" ).length ) { - this.uiDialog.attr( { - "aria-describedby": this.element.uniqueId().attr( "id" ) + that.parentData = { + element: $( document ), + left: 0, + top: 0, + width: $( document ).width(), + height: $( document ).height() || document.body.parentNode.scrollHeight + }; + } else { + element = $( ce ); + p = []; + $( [ "Top", "Right", "Left", "Bottom" ] ).each( function( i, name ) { + p[ i ] = that._num( element.css( "padding" + name ) ); } ); + + that.containerOffset = element.offset(); + that.containerPosition = element.position(); + that.containerSize = { + height: ( element.innerHeight() - p[ 3 ] ), + width: ( element.innerWidth() - p[ 1 ] ) + }; + + co = that.containerOffset; + ch = that.containerSize.height; + cw = that.containerSize.width; + width = ( that._hasScroll( ce, "left" ) ? ce.scrollWidth : cw ); + height = ( that._hasScroll( ce ) ? ce.scrollHeight : ch ); + + that.parentData = { + element: ce, + left: co.left, + top: co.top, + width: width, + height: height + }; } }, - _createTitlebar: function() { - var uiDialogTitle; + resize: function( event ) { + var woset, hoset, isParent, isOffsetRelative, + that = $( this ).resizable( "instance" ), + o = that.options, + co = that.containerOffset, + cp = that.position, + pRatio = that._aspectRatio || event.shiftKey, + cop = { + top: 0, + left: 0 + }, + ce = that.containerElement, + continueResize = true; - this.uiDialogTitlebar = $( "<div>" ); - this._addClass( this.uiDialogTitlebar, - "ui-dialog-titlebar", "ui-widget-header ui-helper-clearfix" ); - this._on( this.uiDialogTitlebar, { - mousedown: function( event ) { + if ( ce[ 0 ] !== document && ( /static/ ).test( ce.css( "position" ) ) ) { + cop = co; + } - // Don't prevent click on close button (#8838) - // Focusing a dialog that is partially scrolled out of view - // causes the browser to scroll it into view, preventing the click event - if ( !$( event.target ).closest( ".ui-dialog-titlebar-close" ) ) { + if ( cp.left < ( that._helper ? co.left : 0 ) ) { + that.size.width = that.size.width + + ( that._helper ? + ( that.position.left - co.left ) : + ( that.position.left - cop.left ) ); - // Dialog isn't getting focus when dragging (#8063) - this.uiDialog.trigger( "focus" ); - } + if ( pRatio ) { + that.size.height = that.size.width / that.aspectRatio; + continueResize = false; } - } ); + that.position.left = o.helper ? co.left : 0; + } - // Support: IE - // Use type="button" to prevent enter keypresses in textboxes from closing the - // dialog in IE (#9312) - this.uiDialogTitlebarClose = $( "<button type='button'></button>" ) - .button( { - label: $( "<a>" ).text( this.options.closeText ).html(), - icon: "ui-icon-closethick", - showLabel: false - } ) - .appendTo( this.uiDialogTitlebar ); + if ( cp.top < ( that._helper ? co.top : 0 ) ) { + that.size.height = that.size.height + + ( that._helper ? + ( that.position.top - co.top ) : + that.position.top ); - this._addClass( this.uiDialogTitlebarClose, "ui-dialog-titlebar-close" ); - this._on( this.uiDialogTitlebarClose, { - click: function( event ) { - event.preventDefault(); - this.close( event ); + if ( pRatio ) { + that.size.width = that.size.height * that.aspectRatio; + continueResize = false; } - } ); - - uiDialogTitle = $( "<span>" ).uniqueId().prependTo( this.uiDialogTitlebar ); - this._addClass( uiDialogTitle, "ui-dialog-title" ); - this._title( uiDialogTitle ); - - this.uiDialogTitlebar.prependTo( this.uiDialog ); + that.position.top = that._helper ? co.top : 0; + } - this.uiDialog.attr( { - "aria-labelledby": uiDialogTitle.attr( "id" ) - } ); - }, + isParent = that.containerElement.get( 0 ) === that.element.parent().get( 0 ); + isOffsetRelative = /relative|absolute/.test( that.containerElement.css( "position" ) ); - _title: function( title ) { - if ( this.options.title ) { - title.text( this.options.title ); + if ( isParent && isOffsetRelative ) { + that.offset.left = that.parentData.left + that.position.left; + that.offset.top = that.parentData.top + that.position.top; } else { - title.html( " " ); + that.offset.left = that.element.offset().left; + that.offset.top = that.element.offset().top; } - }, - _createButtonPane: function() { - this.uiDialogButtonPane = $( "<div>" ); - this._addClass( this.uiDialogButtonPane, "ui-dialog-buttonpane", - "ui-widget-content ui-helper-clearfix" ); + woset = Math.abs( that.sizeDiff.width + + ( that._helper ? + that.offset.left - cop.left : + ( that.offset.left - co.left ) ) ); - this.uiButtonSet = $( "<div>" ) - .appendTo( this.uiDialogButtonPane ); - this._addClass( this.uiButtonSet, "ui-dialog-buttonset" ); + hoset = Math.abs( that.sizeDiff.height + + ( that._helper ? + that.offset.top - cop.top : + ( that.offset.top - co.top ) ) ); - this._createButtons(); + if ( woset + that.size.width >= that.parentData.width ) { + that.size.width = that.parentData.width - woset; + if ( pRatio ) { + that.size.height = that.size.width / that.aspectRatio; + continueResize = false; + } + } + + if ( hoset + that.size.height >= that.parentData.height ) { + that.size.height = that.parentData.height - hoset; + if ( pRatio ) { + that.size.width = that.size.height * that.aspectRatio; + continueResize = false; + } + } + + if ( !continueResize ) { + that.position.left = that.prevPosition.left; + that.position.top = that.prevPosition.top; + that.size.width = that.prevSize.width; + that.size.height = that.prevSize.height; + } }, - _createButtons: function() { - var that = this, - buttons = this.options.buttons; + stop: function() { + var that = $( this ).resizable( "instance" ), + o = that.options, + co = that.containerOffset, + cop = that.containerPosition, + ce = that.containerElement, + helper = $( that.helper ), + ho = helper.offset(), + w = helper.outerWidth() - that.sizeDiff.width, + h = helper.outerHeight() - that.sizeDiff.height; - // If we already have a button pane, remove it - this.uiDialogButtonPane.remove(); - this.uiButtonSet.empty(); + if ( that._helper && !o.animate && ( /relative/ ).test( ce.css( "position" ) ) ) { + $( this ).css( { + left: ho.left - cop.left - co.left, + width: w, + height: h + } ); + } - if ( $.isEmptyObject( buttons ) || ( Array.isArray( buttons ) && !buttons.length ) ) { - this._removeClass( this.uiDialog, "ui-dialog-buttons" ); - return; + if ( that._helper && !o.animate && ( /static/ ).test( ce.css( "position" ) ) ) { + $( this ).css( { + left: ho.left - cop.left - co.left, + width: w, + height: h + } ); } + } +} ); - $.each( buttons, function( name, props ) { - var click, buttonOptions; - props = typeof props === "function" ? - { click: props, text: name } : - props; +$.ui.plugin.add( "resizable", "alsoResize", { - // Default to a non-submitting button - props = $.extend( { type: "button" }, props ); + start: function() { + var that = $( this ).resizable( "instance" ), + o = that.options; - // Change the context for the click callback to be the main element - click = props.click; - buttonOptions = { - icon: props.icon, - iconPosition: props.iconPosition, - showLabel: props.showLabel, + $( o.alsoResize ).each( function() { + var el = $( this ); + el.data( "ui-resizable-alsoresize", { + width: parseFloat( el.width() ), height: parseFloat( el.height() ), + left: parseFloat( el.css( "left" ) ), top: parseFloat( el.css( "top" ) ) + } ); + } ); + }, - // Deprecated options - icons: props.icons, - text: props.text + resize: function( event, ui ) { + var that = $( this ).resizable( "instance" ), + o = that.options, + os = that.originalSize, + op = that.originalPosition, + delta = { + height: ( that.size.height - os.height ) || 0, + width: ( that.size.width - os.width ) || 0, + top: ( that.position.top - op.top ) || 0, + left: ( that.position.left - op.left ) || 0 }; - delete props.click; - delete props.icon; - delete props.iconPosition; - delete props.showLabel; - - // Deprecated options - delete props.icons; - if ( typeof props.text === "boolean" ) { - delete props.text; - } + $( o.alsoResize ).each( function() { + var el = $( this ), start = $( this ).data( "ui-resizable-alsoresize" ), style = {}, + css = el.parents( ui.originalElement[ 0 ] ).length ? + [ "width", "height" ] : + [ "width", "height", "top", "left" ]; - $( "<button></button>", props ) - .button( buttonOptions ) - .appendTo( that.uiButtonSet ) - .on( "click", function() { - click.apply( that.element[ 0 ], arguments ); + $.each( css, function( i, prop ) { + var sum = ( start[ prop ] || 0 ) + ( delta[ prop ] || 0 ); + if ( sum && sum >= 0 ) { + style[ prop ] = sum || null; + } } ); - } ); - this._addClass( this.uiDialog, "ui-dialog-buttons" ); - this.uiDialogButtonPane.appendTo( this.uiDialog ); + + el.css( style ); + } ); }, - _makeDraggable: function() { - var that = this, - options = this.options; + stop: function() { + $( this ).removeData( "ui-resizable-alsoresize" ); + } +} ); - function filteredUi( ui ) { - return { - position: ui.position, - offset: ui.offset - }; - } +$.ui.plugin.add( "resizable", "ghost", { - this.uiDialog.draggable( { - cancel: ".ui-dialog-content, .ui-dialog-titlebar-close", - handle: ".ui-dialog-titlebar", - containment: "document", - start: function( event, ui ) { - that._addClass( $( this ), "ui-dialog-dragging" ); - that._blockFrames(); - that._trigger( "dragStart", event, filteredUi( ui ) ); - }, - drag: function( event, ui ) { - that._trigger( "drag", event, filteredUi( ui ) ); - }, - stop: function( event, ui ) { - var left = ui.offset.left - that.document.scrollLeft(), - top = ui.offset.top - that.document.scrollTop(); + start: function() { - options.position = { - my: "left top", - at: "left" + ( left >= 0 ? "+" : "" ) + left + " " + - "top" + ( top >= 0 ? "+" : "" ) + top, - of: that.window - }; - that._removeClass( $( this ), "ui-dialog-dragging" ); - that._unblockFrames(); - that._trigger( "dragStop", event, filteredUi( ui ) ); - } + var that = $( this ).resizable( "instance" ), cs = that.size; + + that.ghost = that.originalElement.clone(); + that.ghost.css( { + opacity: 0.25, + display: "block", + position: "relative", + height: cs.height, + width: cs.width, + margin: 0, + left: 0, + top: 0 } ); - }, - _makeResizable: function() { - var that = this, - options = this.options, - handles = options.resizable, + that._addClass( that.ghost, "ui-resizable-ghost" ); - // .ui-resizable has position: relative defined in the stylesheet - // but dialogs have to use absolute or fixed positioning - position = this.uiDialog.css( "position" ), - resizeHandles = typeof handles === "string" ? - handles : - "n,e,s,w,se,sw,ne,nw"; + // DEPRECATED + // TODO: remove after 1.12 + if ( $.uiBackCompat !== false && typeof that.options.ghost === "string" ) { - function filteredUi( ui ) { - return { - originalPosition: ui.originalPosition, - originalSize: ui.originalSize, - position: ui.position, - size: ui.size - }; + // Ghost option + that.ghost.addClass( this.options.ghost ); } - this.uiDialog.resizable( { - cancel: ".ui-dialog-content", - containment: "document", - alsoResize: this.element, - maxWidth: options.maxWidth, - maxHeight: options.maxHeight, - minWidth: options.minWidth, - minHeight: this._minHeight(), - handles: resizeHandles, - start: function( event, ui ) { - that._addClass( $( this ), "ui-dialog-resizing" ); - that._blockFrames(); - that._trigger( "resizeStart", event, filteredUi( ui ) ); - }, - resize: function( event, ui ) { - that._trigger( "resize", event, filteredUi( ui ) ); - }, - stop: function( event, ui ) { - var offset = that.uiDialog.offset(), - left = offset.left - that.document.scrollLeft(), - top = offset.top - that.document.scrollTop(); - - options.height = that.uiDialog.height(); - options.width = that.uiDialog.width(); - options.position = { - my: "left top", - at: "left" + ( left >= 0 ? "+" : "" ) + left + " " + - "top" + ( top >= 0 ? "+" : "" ) + top, - of: that.window - }; - that._removeClass( $( this ), "ui-dialog-resizing" ); - that._unblockFrames(); - that._trigger( "resizeStop", event, filteredUi( ui ) ); - } - } ) - .css( "position", position ); - }, - - _trackFocus: function() { - this._on( this.widget(), { - focusin: function( event ) { - this._makeFocusTarget(); - this._focusedElement = $( event.target ); - } - } ); - }, + that.ghost.appendTo( that.helper ); - _makeFocusTarget: function() { - this._untrackInstance(); - this._trackingInstances().unshift( this ); }, - _untrackInstance: function() { - var instances = this._trackingInstances(), - exists = $.inArray( this, instances ); - if ( exists !== -1 ) { - instances.splice( exists, 1 ); + resize: function() { + var that = $( this ).resizable( "instance" ); + if ( that.ghost ) { + that.ghost.css( { + position: "relative", + height: that.size.height, + width: that.size.width + } ); } }, - _trackingInstances: function() { - var instances = this.document.data( "ui-dialog-instances" ); - if ( !instances ) { - instances = []; - this.document.data( "ui-dialog-instances", instances ); + stop: function() { + var that = $( this ).resizable( "instance" ); + if ( that.ghost && that.helper ) { + that.helper.get( 0 ).removeChild( that.ghost.get( 0 ) ); } - return instances; - }, + } - _minHeight: function() { - var options = this.options; +} ); - return options.height === "auto" ? - options.minHeight : - Math.min( options.minHeight, options.height ); - }, +$.ui.plugin.add( "resizable", "grid", { - _position: function() { + resize: function() { + var outerDimensions, + that = $( this ).resizable( "instance" ), + o = that.options, + cs = that.size, + os = that.originalSize, + op = that.originalPosition, + a = that.axis, + grid = typeof o.grid === "number" ? [ o.grid, o.grid ] : o.grid, + gridX = ( grid[ 0 ] || 1 ), + gridY = ( grid[ 1 ] || 1 ), + ox = Math.round( ( cs.width - os.width ) / gridX ) * gridX, + oy = Math.round( ( cs.height - os.height ) / gridY ) * gridY, + newWidth = os.width + ox, + newHeight = os.height + oy, + isMaxWidth = o.maxWidth && ( o.maxWidth < newWidth ), + isMaxHeight = o.maxHeight && ( o.maxHeight < newHeight ), + isMinWidth = o.minWidth && ( o.minWidth > newWidth ), + isMinHeight = o.minHeight && ( o.minHeight > newHeight ); - // Need to show the dialog to get the actual offset in the position plugin - var isVisible = this.uiDialog.is( ":visible" ); - if ( !isVisible ) { - this.uiDialog.show(); + o.grid = grid; + + if ( isMinWidth ) { + newWidth += gridX; } - this.uiDialog.position( this.options.position ); - if ( !isVisible ) { - this.uiDialog.hide(); + if ( isMinHeight ) { + newHeight += gridY; + } + if ( isMaxWidth ) { + newWidth -= gridX; + } + if ( isMaxHeight ) { + newHeight -= gridY; } - }, - - _setOptions: function( options ) { - var that = this, - resize = false, - resizableOptions = {}; - $.each( options, function( key, value ) { - that._setOption( key, value ); + if ( /^(se|s|e)$/.test( a ) ) { + that.size.width = newWidth; + that.size.height = newHeight; + } else if ( /^(ne)$/.test( a ) ) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.top = op.top - oy; + } else if ( /^(sw)$/.test( a ) ) { + that.size.width = newWidth; + that.size.height = newHeight; + that.position.left = op.left - ox; + } else { + if ( newHeight - gridY <= 0 || newWidth - gridX <= 0 ) { + outerDimensions = that._getPaddingPlusBorderDimensions( this ); + } - if ( key in that.sizeRelatedOptions ) { - resize = true; + if ( newHeight - gridY > 0 ) { + that.size.height = newHeight; + that.position.top = op.top - oy; + } else { + newHeight = gridY - outerDimensions.height; + that.size.height = newHeight; + that.position.top = op.top + os.height - newHeight; } - if ( key in that.resizableRelatedOptions ) { - resizableOptions[ key ] = value; + if ( newWidth - gridX > 0 ) { + that.size.width = newWidth; + that.position.left = op.left - ox; + } else { + newWidth = gridX - outerDimensions.width; + that.size.width = newWidth; + that.position.left = op.left + os.width - newWidth; } - } ); - - if ( resize ) { - this._size(); - this._position(); - } - if ( this.uiDialog.is( ":data(ui-resizable)" ) ) { - this.uiDialog.resizable( "option", resizableOptions ); } - }, - - _setOption: function( key, value ) { - var isDraggable, isResizable, - uiDialog = this.uiDialog; + } - if ( key === "disabled" ) { - return; - } +} ); - this._super( key, value ); +var widgetsResizable = $.ui.resizable; - if ( key === "appendTo" ) { - this.uiDialog.appendTo( this._appendTo() ); - } - if ( key === "buttons" ) { - this._createButtons(); - } +/*! + * jQuery UI Dialog 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( key === "closeText" ) { - this.uiDialogTitlebarClose.button( { +//>>label: Dialog +//>>group: Widgets +//>>description: Displays customizable dialog windows. +//>>docs: http://api.jqueryui.com/dialog/ +//>>demos: http://jqueryui.com/dialog/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/dialog.css +//>>css.theme: ../../themes/base/theme.css - // Ensure that we always pass a string - label: $( "<a>" ).text( "" + this.options.closeText ).html() - } ); - } - if ( key === "draggable" ) { - isDraggable = uiDialog.is( ":data(ui-draggable)" ); - if ( isDraggable && !value ) { - uiDialog.draggable( "destroy" ); - } +$.widget( "ui.dialog", { + version: "1.13.2", + options: { + appendTo: "body", + autoOpen: true, + buttons: [], + classes: { + "ui-dialog": "ui-corner-all", + "ui-dialog-titlebar": "ui-corner-all" + }, + closeOnEscape: true, + closeText: "Close", + draggable: true, + hide: null, + height: "auto", + maxHeight: null, + maxWidth: null, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: "center", + at: "center", + of: window, + collision: "fit", - if ( !isDraggable && value ) { - this._makeDraggable(); + // Ensure the titlebar is always visible + using: function( pos ) { + var topOffset = $( this ).css( pos ).offset().top; + if ( topOffset < 0 ) { + $( this ).css( "top", pos.top - topOffset ); + } } - } - - if ( key === "position" ) { - this._position(); - } + }, + resizable: true, + show: null, + title: null, + width: 300, - if ( key === "resizable" ) { + // Callbacks + beforeClose: null, + close: null, + drag: null, + dragStart: null, + dragStop: null, + focus: null, + open: null, + resize: null, + resizeStart: null, + resizeStop: null + }, - // currently resizable, becoming non-resizable - isResizable = uiDialog.is( ":data(ui-resizable)" ); - if ( isResizable && !value ) { - uiDialog.resizable( "destroy" ); - } + sizeRelatedOptions: { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, - // Currently resizable, changing handles - if ( isResizable && typeof value === "string" ) { - uiDialog.resizable( "option", "handles", value ); - } + resizableRelatedOptions: { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }, - // Currently non-resizable, becoming resizable - if ( !isResizable && value !== false ) { - this._makeResizable(); - } + _create: function() { + this.originalCss = { + display: this.element[ 0 ].style.display, + width: this.element[ 0 ].style.width, + minHeight: this.element[ 0 ].style.minHeight, + maxHeight: this.element[ 0 ].style.maxHeight, + height: this.element[ 0 ].style.height + }; + this.originalPosition = { + parent: this.element.parent(), + index: this.element.parent().children().index( this.element ) + }; + this.originalTitle = this.element.attr( "title" ); + if ( this.options.title == null && this.originalTitle != null ) { + this.options.title = this.originalTitle; } - if ( key === "title" ) { - this._title( this.uiDialogTitlebar.find( ".ui-dialog-title" ) ); + // Dialogs can't be disabled + if ( this.options.disabled ) { + this.options.disabled = false; } - }, - _size: function() { - - // If the user has resized the dialog, the .ui-dialog and .ui-dialog-content - // divs will both have width and height set, so we need to reset them - var nonContentHeight, minContentHeight, maxContentHeight, - options = this.options; + this._createWrapper(); - // Reset content sizing - this.element.show().css( { - width: "auto", - minHeight: 0, - maxHeight: "none", - height: 0 - } ); + this.element + .show() + .removeAttr( "title" ) + .appendTo( this.uiDialog ); - if ( options.minWidth > options.width ) { - options.width = options.minWidth; - } + this._addClass( "ui-dialog-content", "ui-widget-content" ); - // Reset wrapper sizing - // determine the height of all the non-content elements - nonContentHeight = this.uiDialog.css( { - height: "auto", - width: options.width - } ) - .outerHeight(); - minContentHeight = Math.max( 0, options.minHeight - nonContentHeight ); - maxContentHeight = typeof options.maxHeight === "number" ? - Math.max( 0, options.maxHeight - nonContentHeight ) : - "none"; + this._createTitlebar(); + this._createButtonPane(); - if ( options.height === "auto" ) { - this.element.css( { - minHeight: minContentHeight, - maxHeight: maxContentHeight, - height: "auto" - } ); - } else { - this.element.height( Math.max( 0, options.height - nonContentHeight ) ); + if ( this.options.draggable && $.fn.draggable ) { + this._makeDraggable(); } - - if ( this.uiDialog.is( ":data(ui-resizable)" ) ) { - this.uiDialog.resizable( "option", "minHeight", this._minHeight() ); + if ( this.options.resizable && $.fn.resizable ) { + this._makeResizable(); } - }, - _blockFrames: function() { - this.iframeBlocks = this.document.find( "iframe" ).map( function() { - var iframe = $( this ); + this._isOpen = false; - return $( "<div>" ) - .css( { - position: "absolute", - width: iframe.outerWidth(), - height: iframe.outerHeight() - } ) - .appendTo( iframe.parent() ) - .offset( iframe.offset() )[ 0 ]; - } ); + this._trackFocus(); }, - _unblockFrames: function() { - if ( this.iframeBlocks ) { - this.iframeBlocks.remove(); - delete this.iframeBlocks; + _init: function() { + if ( this.options.autoOpen ) { + this.open(); } }, - _allowInteraction: function( event ) { - if ( $( event.target ).closest( ".ui-dialog" ).length ) { - return true; + _appendTo: function() { + var element = this.options.appendTo; + if ( element && ( element.jquery || element.nodeType ) ) { + return $( element ); } - - // TODO: Remove hack when datepicker implements - // the .ui-front logic (#8989) - return !!$( event.target ).closest( ".ui-datepicker" ).length; + return this.document.find( element || "body" ).eq( 0 ); }, - _createOverlay: function() { - if ( !this.options.modal ) { - return; - } - - var jqMinor = $.fn.jquery.substring( 0, 4 ); - - // We use a delay in case the overlay is created from an - // event that we're going to be cancelling (#2804) - var isOpening = true; - this._delay( function() { - isOpening = false; - } ); + _destroy: function() { + var next, + originalPosition = this.originalPosition; - if ( !this.document.data( "ui-dialog-overlays" ) ) { + this._untrackInstance(); + this._destroyOverlay(); - // Prevent use of anchors and inputs - // This doesn't use `_on()` because it is a shared event handler - // across all open modal dialogs. - this.document.on( "focusin.ui-dialog", function( event ) { - if ( isOpening ) { - return; - } + this.element + .removeUniqueId() + .css( this.originalCss ) - var instance = this._trackingInstances()[ 0 ]; - if ( !instance._allowInteraction( event ) ) { - event.preventDefault(); - instance._focusTabbable(); + // Without detaching first, the following becomes really slow + .detach(); - // Support: jQuery >=3.4 <3.6 only - // Focus re-triggering in jQuery 3.4/3.5 makes the original element - // have its focus event propagated last, breaking the re-targeting. - // Trigger focus in a delay in addition if needed to avoid the issue - // See https://github.com/jquery/jquery/issues/4382 - if ( jqMinor === "3.4." || jqMinor === "3.5." ) { - instance._delay( instance._restoreTabbableFocus ); - } - } - }.bind( this ) ); + this.uiDialog.remove(); + + if ( this.originalTitle ) { + this.element.attr( "title", this.originalTitle ); } - this.overlay = $( "<div>" ) - .appendTo( this._appendTo() ); + next = originalPosition.parent.children().eq( originalPosition.index ); - this._addClass( this.overlay, null, "ui-widget-overlay ui-front" ); - this._on( this.overlay, { - mousedown: "_keepFocus" - } ); - this.document.data( "ui-dialog-overlays", - ( this.document.data( "ui-dialog-overlays" ) || 0 ) + 1 ); + // Don't try to place the dialog next to itself (#8613) + if ( next.length && next[ 0 ] !== this.element[ 0 ] ) { + next.before( this.element ); + } else { + originalPosition.parent.append( this.element ); + } }, - _destroyOverlay: function() { - if ( !this.options.modal ) { - return; - } + widget: function() { + return this.uiDialog; + }, - if ( this.overlay ) { - var overlays = this.document.data( "ui-dialog-overlays" ) - 1; + disable: $.noop, + enable: $.noop, - if ( !overlays ) { - this.document.off( "focusin.ui-dialog" ); - this.document.removeData( "ui-dialog-overlays" ); - } else { - this.document.data( "ui-dialog-overlays", overlays ); - } + close: function( event ) { + var that = this; - this.overlay.remove(); - this.overlay = null; + if ( !this._isOpen || this._trigger( "beforeClose", event ) === false ) { + return; } - } -} ); -// DEPRECATED -// TODO: switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { + this._isOpen = false; + this._focusedElement = null; + this._destroyOverlay(); + this._untrackInstance(); - // Backcompat for dialogClass option - $.widget( "ui.dialog", $.ui.dialog, { - options: { - dialogClass: "" - }, - _createWrapper: function() { - this._super(); - this.uiDialog.addClass( this.options.dialogClass ); - }, - _setOption: function( key, value ) { - if ( key === "dialogClass" ) { - this.uiDialog - .removeClass( this.options.dialogClass ) - .addClass( value ); - } - this._superApply( arguments ); - } - } ); -} + if ( !this.opener.filter( ":focusable" ).trigger( "focus" ).length ) { -var widgetsDialog = $.ui.dialog; + // Hiding a focused element doesn't trigger blur in WebKit + // so in case we have nothing to focus on, explicitly blur the active element + // https://bugs.webkit.org/show_bug.cgi?id=47182 + $.ui.safeBlur( $.ui.safeActiveElement( this.document[ 0 ] ) ); + } + this._hide( this.uiDialog, this.options.hide, function() { + that._trigger( "close", event ); + } ); + }, -/*! - * jQuery UI Progressbar 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + isOpen: function() { + return this._isOpen; + }, -//>>label: Progressbar -//>>group: Widgets -/* eslint-disable max-len */ -//>>description: Displays a status indicator for loading state, standard percentage, and other progress indicators. -/* eslint-enable max-len */ -//>>docs: http://api.jqueryui.com/progressbar/ -//>>demos: http://jqueryui.com/progressbar/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/progressbar.css -//>>css.theme: ../../themes/base/theme.css + moveToTop: function() { + this._moveToTop(); + }, + _moveToTop: function( event, silent ) { + var moved = false, + zIndices = this.uiDialog.siblings( ".ui-front:visible" ).map( function() { + return +$( this ).css( "z-index" ); + } ).get(), + zIndexMax = Math.max.apply( null, zIndices ); -var widgetsProgressbar = $.widget( "ui.progressbar", { - version: "1.13.1", - options: { - classes: { - "ui-progressbar": "ui-corner-all", - "ui-progressbar-value": "ui-corner-left", - "ui-progressbar-complete": "ui-corner-right" - }, - max: 100, - value: 0, + if ( zIndexMax >= +this.uiDialog.css( "z-index" ) ) { + this.uiDialog.css( "z-index", zIndexMax + 1 ); + moved = true; + } - change: null, - complete: null + if ( moved && !silent ) { + this._trigger( "focus", event ); + } + return moved; }, - min: 0, + open: function() { + var that = this; + if ( this._isOpen ) { + if ( this._moveToTop() ) { + this._focusTabbable(); + } + return; + } - _create: function() { + this._isOpen = true; + this.opener = $( $.ui.safeActiveElement( this.document[ 0 ] ) ); - // Constrain initial value - this.oldValue = this.options.value = this._constrainedValue(); + this._size(); + this._position(); + this._createOverlay(); + this._moveToTop( null, true ); - this.element.attr( { + // Ensure the overlay is moved to the top with the dialog, but only when + // opening. The overlay shouldn't move after the dialog is open so that + // modeless dialogs opened after the modal dialog stack properly. + if ( this.overlay ) { + this.overlay.css( "z-index", this.uiDialog.css( "z-index" ) - 1 ); + } - // Only set static values; aria-valuenow and aria-valuemax are - // set inside _refreshValue() - role: "progressbar", - "aria-valuemin": this.min + this._show( this.uiDialog, this.options.show, function() { + that._focusTabbable(); + that._trigger( "focus" ); } ); - this._addClass( "ui-progressbar", "ui-widget ui-widget-content" ); - - this.valueDiv = $( "<div>" ).appendTo( this.element ); - this._addClass( this.valueDiv, "ui-progressbar-value", "ui-widget-header" ); - this._refreshValue(); - }, - _destroy: function() { - this.element.removeAttr( "role aria-valuemin aria-valuemax aria-valuenow" ); + // Track the dialog immediately upon opening in case a focus event + // somehow occurs outside of the dialog before an element inside the + // dialog is focused (#10152) + this._makeFocusTarget(); - this.valueDiv.remove(); + this._trigger( "open" ); }, - value: function( newValue ) { - if ( newValue === undefined ) { - return this.options.value; - } + _focusTabbable: function() { - this.options.value = this._constrainedValue( newValue ); - this._refreshValue(); + // Set focus to the first match: + // 1. An element that was focused previously + // 2. First element inside the dialog matching [autofocus] + // 3. Tabbable element inside the content element + // 4. Tabbable element inside the buttonpane + // 5. The close button + // 6. The dialog itself + var hasFocus = this._focusedElement; + if ( !hasFocus ) { + hasFocus = this.element.find( "[autofocus]" ); + } + if ( !hasFocus.length ) { + hasFocus = this.element.find( ":tabbable" ); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialogButtonPane.find( ":tabbable" ); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialogTitlebarClose.filter( ":tabbable" ); + } + if ( !hasFocus.length ) { + hasFocus = this.uiDialog; + } + hasFocus.eq( 0 ).trigger( "focus" ); }, - _constrainedValue: function( newValue ) { - if ( newValue === undefined ) { - newValue = this.options.value; + _restoreTabbableFocus: function() { + var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ), + isActive = this.uiDialog[ 0 ] === activeElement || + $.contains( this.uiDialog[ 0 ], activeElement ); + if ( !isActive ) { + this._focusTabbable(); } + }, - this.indeterminate = newValue === false; - - // Sanitize value - if ( typeof newValue !== "number" ) { - newValue = 0; - } + _keepFocus: function( event ) { + event.preventDefault(); + this._restoreTabbableFocus(); - return this.indeterminate ? false : - Math.min( this.options.max, Math.max( this.min, newValue ) ); + // support: IE + // IE <= 8 doesn't prevent moving focus even with event.preventDefault() + // so we check again later + this._delay( this._restoreTabbableFocus ); }, - _setOptions: function( options ) { + _createWrapper: function() { + this.uiDialog = $( "<div>" ) + .hide() + .attr( { - // Ensure "value" option is set after other values (like max) - var value = options.value; - delete options.value; + // Setting tabIndex makes the div focusable + tabIndex: -1, + role: "dialog" + } ) + .appendTo( this._appendTo() ); - this._super( options ); + this._addClass( this.uiDialog, "ui-dialog", "ui-widget ui-widget-content ui-front" ); + this._on( this.uiDialog, { + keydown: function( event ) { + if ( this.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE ) { + event.preventDefault(); + this.close( event ); + return; + } - this.options.value = this._constrainedValue( value ); - this._refreshValue(); - }, + // Prevent tabbing out of dialogs + if ( event.keyCode !== $.ui.keyCode.TAB || event.isDefaultPrevented() ) { + return; + } + var tabbables = this.uiDialog.find( ":tabbable" ), + first = tabbables.first(), + last = tabbables.last(); - _setOption: function( key, value ) { - if ( key === "max" ) { + if ( ( event.target === last[ 0 ] || event.target === this.uiDialog[ 0 ] ) && + !event.shiftKey ) { + this._delay( function() { + first.trigger( "focus" ); + } ); + event.preventDefault(); + } else if ( ( event.target === first[ 0 ] || + event.target === this.uiDialog[ 0 ] ) && event.shiftKey ) { + this._delay( function() { + last.trigger( "focus" ); + } ); + event.preventDefault(); + } + }, + mousedown: function( event ) { + if ( this._moveToTop( event ) ) { + this._focusTabbable(); + } + } + } ); - // Don't allow a max less than min - value = Math.max( this.min, value ); + // We assume that any existing aria-describedby attribute means + // that the dialog content is marked up properly + // otherwise we brute force the content as the description + if ( !this.element.find( "[aria-describedby]" ).length ) { + this.uiDialog.attr( { + "aria-describedby": this.element.uniqueId().attr( "id" ) + } ); } - this._super( key, value ); }, - _setOptionDisabled: function( value ) { - this._super( value ); - - this.element.attr( "aria-disabled", value ); - this._toggleClass( null, "ui-state-disabled", !!value ); - }, + _createTitlebar: function() { + var uiDialogTitle; - _percentage: function() { - return this.indeterminate ? - 100 : - 100 * ( this.options.value - this.min ) / ( this.options.max - this.min ); - }, + this.uiDialogTitlebar = $( "<div>" ); + this._addClass( this.uiDialogTitlebar, + "ui-dialog-titlebar", "ui-widget-header ui-helper-clearfix" ); + this._on( this.uiDialogTitlebar, { + mousedown: function( event ) { - _refreshValue: function() { - var value = this.options.value, - percentage = this._percentage(); + // Don't prevent click on close button (#8838) + // Focusing a dialog that is partially scrolled out of view + // causes the browser to scroll it into view, preventing the click event + if ( !$( event.target ).closest( ".ui-dialog-titlebar-close" ) ) { - this.valueDiv - .toggle( this.indeterminate || value > this.min ) - .width( percentage.toFixed( 0 ) + "%" ); + // Dialog isn't getting focus when dragging (#8063) + this.uiDialog.trigger( "focus" ); + } + } + } ); - this - ._toggleClass( this.valueDiv, "ui-progressbar-complete", null, - value === this.options.max ) - ._toggleClass( "ui-progressbar-indeterminate", null, this.indeterminate ); + // Support: IE + // Use type="button" to prevent enter keypresses in textboxes from closing the + // dialog in IE (#9312) + this.uiDialogTitlebarClose = $( "<button type='button'></button>" ) + .button( { + label: $( "<a>" ).text( this.options.closeText ).html(), + icon: "ui-icon-closethick", + showLabel: false + } ) + .appendTo( this.uiDialogTitlebar ); - if ( this.indeterminate ) { - this.element.removeAttr( "aria-valuenow" ); - if ( !this.overlayDiv ) { - this.overlayDiv = $( "<div>" ).appendTo( this.valueDiv ); - this._addClass( this.overlayDiv, "ui-progressbar-overlay" ); - } - } else { - this.element.attr( { - "aria-valuemax": this.options.max, - "aria-valuenow": value - } ); - if ( this.overlayDiv ) { - this.overlayDiv.remove(); - this.overlayDiv = null; + this._addClass( this.uiDialogTitlebarClose, "ui-dialog-titlebar-close" ); + this._on( this.uiDialogTitlebarClose, { + click: function( event ) { + event.preventDefault(); + this.close( event ); } - } + } ); - if ( this.oldValue !== value ) { - this.oldValue = value; - this._trigger( "change" ); - } - if ( value === this.options.max ) { - this._trigger( "complete" ); - } - } -} ); + uiDialogTitle = $( "<span>" ).uniqueId().prependTo( this.uiDialogTitlebar ); + this._addClass( uiDialogTitle, "ui-dialog-title" ); + this._title( uiDialogTitle ); + this.uiDialogTitlebar.prependTo( this.uiDialog ); -/*! - * jQuery UI Selectmenu 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this.uiDialog.attr( { + "aria-labelledby": uiDialogTitle.attr( "id" ) + } ); + }, -//>>label: Selectmenu -//>>group: Widgets -/* eslint-disable max-len */ -//>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select. -/* eslint-enable max-len */ -//>>docs: http://api.jqueryui.com/selectmenu/ -//>>demos: http://jqueryui.com/selectmenu/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/selectmenu.css, ../../themes/base/button.css -//>>css.theme: ../../themes/base/theme.css + _title: function( title ) { + if ( this.options.title ) { + title.text( this.options.title ); + } else { + title.html( " " ); + } + }, + _createButtonPane: function() { + this.uiDialogButtonPane = $( "<div>" ); + this._addClass( this.uiDialogButtonPane, "ui-dialog-buttonpane", + "ui-widget-content ui-helper-clearfix" ); -var widgetsSelectmenu = $.widget( "ui.selectmenu", [ $.ui.formResetMixin, { - version: "1.13.1", - defaultElement: "<select>", - options: { - appendTo: null, - classes: { - "ui-selectmenu-button-open": "ui-corner-top", - "ui-selectmenu-button-closed": "ui-corner-all" - }, - disabled: null, - icons: { - button: "ui-icon-triangle-1-s" - }, - position: { - my: "left top", - at: "left bottom", - collision: "none" - }, - width: false, + this.uiButtonSet = $( "<div>" ) + .appendTo( this.uiDialogButtonPane ); + this._addClass( this.uiButtonSet, "ui-dialog-buttonset" ); - // Callbacks - change: null, - close: null, - focus: null, - open: null, - select: null + this._createButtons(); }, - _create: function() { - var selectmenuId = this.element.uniqueId().attr( "id" ); - this.ids = { - element: selectmenuId, - button: selectmenuId + "-button", - menu: selectmenuId + "-menu" - }; + _createButtons: function() { + var that = this, + buttons = this.options.buttons; - this._drawButton(); - this._drawMenu(); - this._bindFormResetHandler(); + // If we already have a button pane, remove it + this.uiDialogButtonPane.remove(); + this.uiButtonSet.empty(); - this._rendered = false; - this.menuItems = $(); - }, + if ( $.isEmptyObject( buttons ) || ( Array.isArray( buttons ) && !buttons.length ) ) { + this._removeClass( this.uiDialog, "ui-dialog-buttons" ); + return; + } - _drawButton: function() { - var icon, - that = this, - item = this._parseOption( - this.element.find( "option:selected" ), - this.element[ 0 ].selectedIndex - ); + $.each( buttons, function( name, props ) { + var click, buttonOptions; + props = typeof props === "function" ? + { click: props, text: name } : + props; - // Associate existing label with the new button - this.labels = this.element.labels().attr( "for", this.ids.button ); - this._on( this.labels, { - click: function( event ) { - this.button.trigger( "focus" ); - event.preventDefault(); - } - } ); + // Default to a non-submitting button + props = $.extend( { type: "button" }, props ); + + // Change the context for the click callback to be the main element + click = props.click; + buttonOptions = { + icon: props.icon, + iconPosition: props.iconPosition, + showLabel: props.showLabel, - // Hide original select element - this.element.hide(); + // Deprecated options + icons: props.icons, + text: props.text + }; - // Create button - this.button = $( "<span>", { - tabindex: this.options.disabled ? -1 : 0, - id: this.ids.button, - role: "combobox", - "aria-expanded": "false", - "aria-autocomplete": "list", - "aria-owns": this.ids.menu, - "aria-haspopup": "true", - title: this.element.attr( "title" ) - } ) - .insertAfter( this.element ); + delete props.click; + delete props.icon; + delete props.iconPosition; + delete props.showLabel; - this._addClass( this.button, "ui-selectmenu-button ui-selectmenu-button-closed", - "ui-button ui-widget" ); + // Deprecated options + delete props.icons; + if ( typeof props.text === "boolean" ) { + delete props.text; + } - icon = $( "<span>" ).appendTo( this.button ); - this._addClass( icon, "ui-selectmenu-icon", "ui-icon " + this.options.icons.button ); - this.buttonItem = this._renderButtonItem( item ) - .appendTo( this.button ); + $( "<button></button>", props ) + .button( buttonOptions ) + .appendTo( that.uiButtonSet ) + .on( "click", function() { + click.apply( that.element[ 0 ], arguments ); + } ); + } ); + this._addClass( this.uiDialog, "ui-dialog-buttons" ); + this.uiDialogButtonPane.appendTo( this.uiDialog ); + }, - if ( this.options.width !== false ) { - this._resizeButton(); + _makeDraggable: function() { + var that = this, + options = this.options; + + function filteredUi( ui ) { + return { + position: ui.position, + offset: ui.offset + }; } - this._on( this.button, this._buttonEvents ); - this.button.one( "focusin", function() { + this.uiDialog.draggable( { + cancel: ".ui-dialog-content, .ui-dialog-titlebar-close", + handle: ".ui-dialog-titlebar", + containment: "document", + start: function( event, ui ) { + that._addClass( $( this ), "ui-dialog-dragging" ); + that._blockFrames(); + that._trigger( "dragStart", event, filteredUi( ui ) ); + }, + drag: function( event, ui ) { + that._trigger( "drag", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + var left = ui.offset.left - that.document.scrollLeft(), + top = ui.offset.top - that.document.scrollTop(); - // Delay rendering the menu items until the button receives focus. - // The menu may have already been rendered via a programmatic open. - if ( !that._rendered ) { - that._refreshMenu(); + options.position = { + my: "left top", + at: "left" + ( left >= 0 ? "+" : "" ) + left + " " + + "top" + ( top >= 0 ? "+" : "" ) + top, + of: that.window + }; + that._removeClass( $( this ), "ui-dialog-dragging" ); + that._unblockFrames(); + that._trigger( "dragStop", event, filteredUi( ui ) ); } } ); }, - _drawMenu: function() { - var that = this; + _makeResizable: function() { + var that = this, + options = this.options, + handles = options.resizable, - // Create menu - this.menu = $( "<ul>", { - "aria-hidden": "true", - "aria-labelledby": this.ids.button, - id: this.ids.menu - } ); + // .ui-resizable has position: relative defined in the stylesheet + // but dialogs have to use absolute or fixed positioning + position = this.uiDialog.css( "position" ), + resizeHandles = typeof handles === "string" ? + handles : + "n,e,s,w,se,sw,ne,nw"; - // Wrap menu - this.menuWrap = $( "<div>" ).append( this.menu ); - this._addClass( this.menuWrap, "ui-selectmenu-menu", "ui-front" ); - this.menuWrap.appendTo( this._appendTo() ); + function filteredUi( ui ) { + return { + originalPosition: ui.originalPosition, + originalSize: ui.originalSize, + position: ui.position, + size: ui.size + }; + } - // Initialize menu widget - this.menuInstance = this.menu - .menu( { - classes: { - "ui-menu": "ui-corner-bottom" - }, - role: "listbox", - select: function( event, ui ) { - event.preventDefault(); + this.uiDialog.resizable( { + cancel: ".ui-dialog-content", + containment: "document", + alsoResize: this.element, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + minWidth: options.minWidth, + minHeight: this._minHeight(), + handles: resizeHandles, + start: function( event, ui ) { + that._addClass( $( this ), "ui-dialog-resizing" ); + that._blockFrames(); + that._trigger( "resizeStart", event, filteredUi( ui ) ); + }, + resize: function( event, ui ) { + that._trigger( "resize", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + var offset = that.uiDialog.offset(), + left = offset.left - that.document.scrollLeft(), + top = offset.top - that.document.scrollTop(); - // Support: IE8 - // If the item was selected via a click, the text selection - // will be destroyed in IE - that._setSelection(); + options.height = that.uiDialog.height(); + options.width = that.uiDialog.width(); + options.position = { + my: "left top", + at: "left" + ( left >= 0 ? "+" : "" ) + left + " " + + "top" + ( top >= 0 ? "+" : "" ) + top, + of: that.window + }; + that._removeClass( $( this ), "ui-dialog-resizing" ); + that._unblockFrames(); + that._trigger( "resizeStop", event, filteredUi( ui ) ); + } + } ) + .css( "position", position ); + }, - that._select( ui.item.data( "ui-selectmenu-item" ), event ); - }, - focus: function( event, ui ) { - var item = ui.item.data( "ui-selectmenu-item" ); + _trackFocus: function() { + this._on( this.widget(), { + focusin: function( event ) { + this._makeFocusTarget(); + this._focusedElement = $( event.target ); + } + } ); + }, - // Prevent inital focus from firing and check if its a newly focused item - if ( that.focusIndex != null && item.index !== that.focusIndex ) { - that._trigger( "focus", event, { item: item } ); - if ( !that.isOpen ) { - that._select( item, event ); - } - } - that.focusIndex = item.index; + _makeFocusTarget: function() { + this._untrackInstance(); + this._trackingInstances().unshift( this ); + }, - that.button.attr( "aria-activedescendant", - that.menuItems.eq( item.index ).attr( "id" ) ); - } - } ) - .menu( "instance" ); + _untrackInstance: function() { + var instances = this._trackingInstances(), + exists = $.inArray( this, instances ); + if ( exists !== -1 ) { + instances.splice( exists, 1 ); + } + }, - // Don't close the menu on mouseleave - this.menuInstance._off( this.menu, "mouseleave" ); + _trackingInstances: function() { + var instances = this.document.data( "ui-dialog-instances" ); + if ( !instances ) { + instances = []; + this.document.data( "ui-dialog-instances", instances ); + } + return instances; + }, - // Cancel the menu's collapseAll on document click - this.menuInstance._closeOnDocumentClick = function() { - return false; - }; + _minHeight: function() { + var options = this.options; - // Selects often contain empty items, but never contain dividers - this.menuInstance._isDivider = function() { - return false; - }; + return options.height === "auto" ? + options.minHeight : + Math.min( options.minHeight, options.height ); }, - refresh: function() { - this._refreshMenu(); - this.buttonItem.replaceWith( - this.buttonItem = this._renderButtonItem( + _position: function() { - // Fall back to an empty object in case there are no options - this._getSelectedItem().data( "ui-selectmenu-item" ) || {} - ) - ); - if ( this.options.width === null ) { - this._resizeButton(); + // Need to show the dialog to get the actual offset in the position plugin + var isVisible = this.uiDialog.is( ":visible" ); + if ( !isVisible ) { + this.uiDialog.show(); + } + this.uiDialog.position( this.options.position ); + if ( !isVisible ) { + this.uiDialog.hide(); } }, - _refreshMenu: function() { - var item, - options = this.element.find( "option" ); + _setOptions: function( options ) { + var that = this, + resize = false, + resizableOptions = {}; - this.menu.empty(); + $.each( options, function( key, value ) { + that._setOption( key, value ); - this._parseOptions( options ); - this._renderMenu( this.menu, this.items ); + if ( key in that.sizeRelatedOptions ) { + resize = true; + } + if ( key in that.resizableRelatedOptions ) { + resizableOptions[ key ] = value; + } + } ); - this.menuInstance.refresh(); - this.menuItems = this.menu.find( "li" ) - .not( ".ui-selectmenu-optgroup" ) - .find( ".ui-menu-item-wrapper" ); + if ( resize ) { + this._size(); + this._position(); + } + if ( this.uiDialog.is( ":data(ui-resizable)" ) ) { + this.uiDialog.resizable( "option", resizableOptions ); + } + }, - this._rendered = true; + _setOption: function( key, value ) { + var isDraggable, isResizable, + uiDialog = this.uiDialog; - if ( !options.length ) { + if ( key === "disabled" ) { return; } - item = this._getSelectedItem(); + this._super( key, value ); - // Update the menu to have the correct item focused - this.menuInstance.focus( null, item ); - this._setAria( item.data( "ui-selectmenu-item" ) ); + if ( key === "appendTo" ) { + this.uiDialog.appendTo( this._appendTo() ); + } - // Set disabled state - this._setOption( "disabled", this.element.prop( "disabled" ) ); - }, + if ( key === "buttons" ) { + this._createButtons(); + } - open: function( event ) { - if ( this.options.disabled ) { - return; + if ( key === "closeText" ) { + this.uiDialogTitlebarClose.button( { + + // Ensure that we always pass a string + label: $( "<a>" ).text( "" + this.options.closeText ).html() + } ); } - // If this is the first time the menu is being opened, render the items - if ( !this._rendered ) { - this._refreshMenu(); - } else { + if ( key === "draggable" ) { + isDraggable = uiDialog.is( ":data(ui-draggable)" ); + if ( isDraggable && !value ) { + uiDialog.draggable( "destroy" ); + } - // Menu clears focus on close, reset focus to selected item - this._removeClass( this.menu.find( ".ui-state-active" ), null, "ui-state-active" ); - this.menuInstance.focus( null, this._getSelectedItem() ); + if ( !isDraggable && value ) { + this._makeDraggable(); + } } - // If there are no options, don't open the menu - if ( !this.menuItems.length ) { - return; + if ( key === "position" ) { + this._position(); } - this.isOpen = true; - this._toggleAttr(); - this._resizeMenu(); - this._position(); + if ( key === "resizable" ) { - this._on( this.document, this._documentClick ); + // currently resizable, becoming non-resizable + isResizable = uiDialog.is( ":data(ui-resizable)" ); + if ( isResizable && !value ) { + uiDialog.resizable( "destroy" ); + } - this._trigger( "open", event ); - }, + // Currently resizable, changing handles + if ( isResizable && typeof value === "string" ) { + uiDialog.resizable( "option", "handles", value ); + } - _position: function() { - this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) ); - }, + // Currently non-resizable, becoming resizable + if ( !isResizable && value !== false ) { + this._makeResizable(); + } + } - close: function( event ) { - if ( !this.isOpen ) { - return; + if ( key === "title" ) { + this._title( this.uiDialogTitlebar.find( ".ui-dialog-title" ) ); } + }, - this.isOpen = false; - this._toggleAttr(); + _size: function() { - this.range = null; - this._off( this.document ); + // If the user has resized the dialog, the .ui-dialog and .ui-dialog-content + // divs will both have width and height set, so we need to reset them + var nonContentHeight, minContentHeight, maxContentHeight, + options = this.options; - this._trigger( "close", event ); - }, + // Reset content sizing + this.element.show().css( { + width: "auto", + minHeight: 0, + maxHeight: "none", + height: 0 + } ); - widget: function() { - return this.button; - }, + if ( options.minWidth > options.width ) { + options.width = options.minWidth; + } - menuWidget: function() { - return this.menu; - }, + // Reset wrapper sizing + // determine the height of all the non-content elements + nonContentHeight = this.uiDialog.css( { + height: "auto", + width: options.width + } ) + .outerHeight(); + minContentHeight = Math.max( 0, options.minHeight - nonContentHeight ); + maxContentHeight = typeof options.maxHeight === "number" ? + Math.max( 0, options.maxHeight - nonContentHeight ) : + "none"; - _renderButtonItem: function( item ) { - var buttonItem = $( "<span>" ); + if ( options.height === "auto" ) { + this.element.css( { + minHeight: minContentHeight, + maxHeight: maxContentHeight, + height: "auto" + } ); + } else { + this.element.height( Math.max( 0, options.height - nonContentHeight ) ); + } - this._setText( buttonItem, item.label ); - this._addClass( buttonItem, "ui-selectmenu-text" ); + if ( this.uiDialog.is( ":data(ui-resizable)" ) ) { + this.uiDialog.resizable( "option", "minHeight", this._minHeight() ); + } + }, - return buttonItem; + _blockFrames: function() { + this.iframeBlocks = this.document.find( "iframe" ).map( function() { + var iframe = $( this ); + + return $( "<div>" ) + .css( { + position: "absolute", + width: iframe.outerWidth(), + height: iframe.outerHeight() + } ) + .appendTo( iframe.parent() ) + .offset( iframe.offset() )[ 0 ]; + } ); }, - _renderMenu: function( ul, items ) { - var that = this, - currentOptgroup = ""; + _unblockFrames: function() { + if ( this.iframeBlocks ) { + this.iframeBlocks.remove(); + delete this.iframeBlocks; + } + }, - $.each( items, function( index, item ) { - var li; + _allowInteraction: function( event ) { + if ( $( event.target ).closest( ".ui-dialog" ).length ) { + return true; + } - if ( item.optgroup !== currentOptgroup ) { - li = $( "<li>", { - text: item.optgroup - } ); - that._addClass( li, "ui-selectmenu-optgroup", "ui-menu-divider" + - ( item.element.parent( "optgroup" ).prop( "disabled" ) ? - " ui-state-disabled" : - "" ) ); + // TODO: Remove hack when datepicker implements + // the .ui-front logic (#8989) + return !!$( event.target ).closest( ".ui-datepicker" ).length; + }, - li.appendTo( ul ); + _createOverlay: function() { + if ( !this.options.modal ) { + return; + } - currentOptgroup = item.optgroup; - } + var jqMinor = $.fn.jquery.substring( 0, 4 ); - that._renderItemData( ul, item ); + // We use a delay in case the overlay is created from an + // event that we're going to be cancelling (#2804) + var isOpening = true; + this._delay( function() { + isOpening = false; } ); - }, - _renderItemData: function( ul, item ) { - return this._renderItem( ul, item ).data( "ui-selectmenu-item", item ); - }, + if ( !this.document.data( "ui-dialog-overlays" ) ) { - _renderItem: function( ul, item ) { - var li = $( "<li>" ), - wrapper = $( "<div>", { - title: item.element.attr( "title" ) - } ); + // Prevent use of anchors and inputs + // This doesn't use `_on()` because it is a shared event handler + // across all open modal dialogs. + this.document.on( "focusin.ui-dialog", function( event ) { + if ( isOpening ) { + return; + } - if ( item.disabled ) { - this._addClass( li, null, "ui-state-disabled" ); + var instance = this._trackingInstances()[ 0 ]; + if ( !instance._allowInteraction( event ) ) { + event.preventDefault(); + instance._focusTabbable(); + + // Support: jQuery >=3.4 <3.6 only + // Focus re-triggering in jQuery 3.4/3.5 makes the original element + // have its focus event propagated last, breaking the re-targeting. + // Trigger focus in a delay in addition if needed to avoid the issue + // See https://github.com/jquery/jquery/issues/4382 + if ( jqMinor === "3.4." || jqMinor === "3.5." ) { + instance._delay( instance._restoreTabbableFocus ); + } + } + }.bind( this ) ); } - this._setText( wrapper, item.label ); - return li.append( wrapper ).appendTo( ul ); + this.overlay = $( "<div>" ) + .appendTo( this._appendTo() ); + + this._addClass( this.overlay, null, "ui-widget-overlay ui-front" ); + this._on( this.overlay, { + mousedown: "_keepFocus" + } ); + this.document.data( "ui-dialog-overlays", + ( this.document.data( "ui-dialog-overlays" ) || 0 ) + 1 ); }, - _setText: function( element, value ) { - if ( value ) { - element.text( value ); - } else { - element.html( " " ); + _destroyOverlay: function() { + if ( !this.options.modal ) { + return; } - }, - _move: function( direction, event ) { - var item, next, - filter = ".ui-menu-item"; + if ( this.overlay ) { + var overlays = this.document.data( "ui-dialog-overlays" ) - 1; - if ( this.isOpen ) { - item = this.menuItems.eq( this.focusIndex ).parent( "li" ); - } else { - item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" ); - filter += ":not(.ui-state-disabled)"; - } + if ( !overlays ) { + this.document.off( "focusin.ui-dialog" ); + this.document.removeData( "ui-dialog-overlays" ); + } else { + this.document.data( "ui-dialog-overlays", overlays ); + } - if ( direction === "first" || direction === "last" ) { - next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 ); - } else { - next = item[ direction + "All" ]( filter ).eq( 0 ); + this.overlay.remove(); + this.overlay = null; } + } +} ); - if ( next.length ) { - this.menuInstance.focus( event, next ); +// DEPRECATED +// TODO: switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { + + // Backcompat for dialogClass option + $.widget( "ui.dialog", $.ui.dialog, { + options: { + dialogClass: "" + }, + _createWrapper: function() { + this._super(); + this.uiDialog.addClass( this.options.dialogClass ); + }, + _setOption: function( key, value ) { + if ( key === "dialogClass" ) { + this.uiDialog + .removeClass( this.options.dialogClass ) + .addClass( value ); + } + this._superApply( arguments ); } - }, + } ); +} - _getSelectedItem: function() { - return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" ); - }, +var widgetsDialog = $.ui.dialog; - _toggle: function( event ) { - this[ this.isOpen ? "close" : "open" ]( event ); - }, - _setSelection: function() { - var selection; +/*! + * jQuery UI Droppable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( !this.range ) { - return; - } +//>>label: Droppable +//>>group: Interactions +//>>description: Enables drop targets for draggable elements. +//>>docs: http://api.jqueryui.com/droppable/ +//>>demos: http://jqueryui.com/droppable/ - if ( window.getSelection ) { - selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange( this.range ); - // Support: IE8 - } else { - this.range.select(); - } +$.widget( "ui.droppable", { + version: "1.13.2", + widgetEventPrefix: "drop", + options: { + accept: "*", + addClasses: true, + greedy: false, + scope: "default", + tolerance: "intersect", - // Support: IE - // Setting the text selection kills the button focus in IE, but - // restoring the focus doesn't kill the selection. - this.button.focus(); + // Callbacks + activate: null, + deactivate: null, + drop: null, + out: null, + over: null }, + _create: function() { - _documentClick: { - mousedown: function( event ) { - if ( !this.isOpen ) { - return; - } - - if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" + - $.escapeSelector( this.ids.button ) ).length ) { - this.close( event ); - } - } - }, + var proportions, + o = this.options, + accept = o.accept; - _buttonEvents: { + this.isover = false; + this.isout = true; - // Prevent text selection from being reset when interacting with the selectmenu (#10144) - mousedown: function() { - var selection; + this.accept = typeof accept === "function" ? accept : function( d ) { + return d.is( accept ); + }; - if ( window.getSelection ) { - selection = window.getSelection(); - if ( selection.rangeCount ) { - this.range = selection.getRangeAt( 0 ); - } + this.proportions = function( /* valueToWrite */ ) { + if ( arguments.length ) { - // Support: IE8 + // Store the droppable's proportions + proportions = arguments[ 0 ]; } else { - this.range = document.selection.createRange(); - } - }, - - click: function( event ) { - this._setSelection(); - this._toggle( event ); - }, - keydown: function( event ) { - var preventDefault = true; - switch ( event.keyCode ) { - case $.ui.keyCode.TAB: - case $.ui.keyCode.ESCAPE: - this.close( event ); - preventDefault = false; - break; - case $.ui.keyCode.ENTER: - if ( this.isOpen ) { - this._selectFocusedItem( event ); - } - break; - case $.ui.keyCode.UP: - if ( event.altKey ) { - this._toggle( event ); - } else { - this._move( "prev", event ); - } - break; - case $.ui.keyCode.DOWN: - if ( event.altKey ) { - this._toggle( event ); - } else { - this._move( "next", event ); - } - break; - case $.ui.keyCode.SPACE: - if ( this.isOpen ) { - this._selectFocusedItem( event ); - } else { - this._toggle( event ); - } - break; - case $.ui.keyCode.LEFT: - this._move( "prev", event ); - break; - case $.ui.keyCode.RIGHT: - this._move( "next", event ); - break; - case $.ui.keyCode.HOME: - case $.ui.keyCode.PAGE_UP: - this._move( "first", event ); - break; - case $.ui.keyCode.END: - case $.ui.keyCode.PAGE_DOWN: - this._move( "last", event ); - break; - default: - this.menu.trigger( event ); - preventDefault = false; + // Retrieve or derive the droppable's proportions + return proportions ? + proportions : + proportions = { + width: this.element[ 0 ].offsetWidth, + height: this.element[ 0 ].offsetHeight + }; } + }; - if ( preventDefault ) { - event.preventDefault(); - } - } - }, + this._addToManager( o.scope ); - _selectFocusedItem: function( event ) { - var item = this.menuItems.eq( this.focusIndex ).parent( "li" ); - if ( !item.hasClass( "ui-state-disabled" ) ) { - this._select( item.data( "ui-selectmenu-item" ), event ); + if ( o.addClasses ) { + this._addClass( "ui-droppable" ); } + }, - _select: function( item, event ) { - var oldIndex = this.element[ 0 ].selectedIndex; + _addToManager: function( scope ) { - // Change native select element - this.element[ 0 ].selectedIndex = item.index; - this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) ); - this._setAria( item ); - this._trigger( "select", event, { item: item } ); + // Add the reference and positions to the manager + $.ui.ddmanager.droppables[ scope ] = $.ui.ddmanager.droppables[ scope ] || []; + $.ui.ddmanager.droppables[ scope ].push( this ); + }, - if ( item.index !== oldIndex ) { - this._trigger( "change", event, { item: item } ); + _splice: function( drop ) { + var i = 0; + for ( ; i < drop.length; i++ ) { + if ( drop[ i ] === this ) { + drop.splice( i, 1 ); + } } - - this.close( event ); }, - _setAria: function( item ) { - var id = this.menuItems.eq( item.index ).attr( "id" ); + _destroy: function() { + var drop = $.ui.ddmanager.droppables[ this.options.scope ]; - this.button.attr( { - "aria-labelledby": id, - "aria-activedescendant": id - } ); - this.menu.attr( "aria-activedescendant", id ); + this._splice( drop ); }, _setOption: function( key, value ) { - if ( key === "icons" ) { - var icon = this.button.find( "span.ui-icon" ); - this._removeClass( icon, null, this.options.icons.button ) - ._addClass( icon, null, value.button ); + + if ( key === "accept" ) { + this.accept = typeof value === "function" ? value : function( d ) { + return d.is( value ); + }; + } else if ( key === "scope" ) { + var drop = $.ui.ddmanager.droppables[ this.options.scope ]; + + this._splice( drop ); + this._addToManager( value ); } this._super( key, value ); + }, - if ( key === "appendTo" ) { - this.menuWrap.appendTo( this._appendTo() ); - } + _activate: function( event ) { + var draggable = $.ui.ddmanager.current; - if ( key === "width" ) { - this._resizeButton(); + this._addActiveClass(); + if ( draggable ) { + this._trigger( "activate", event, this.ui( draggable ) ); } }, - _setOptionDisabled: function( value ) { - this._super( value ); - - this.menuInstance.option( "disabled", value ); - this.button.attr( "aria-disabled", value ); - this._toggleClass( this.button, null, "ui-state-disabled", value ); + _deactivate: function( event ) { + var draggable = $.ui.ddmanager.current; - this.element.prop( "disabled", value ); - if ( value ) { - this.button.attr( "tabindex", -1 ); - this.close(); - } else { - this.button.attr( "tabindex", 0 ); + this._removeActiveClass(); + if ( draggable ) { + this._trigger( "deactivate", event, this.ui( draggable ) ); } }, - _appendTo: function() { - var element = this.options.appendTo; + _over: function( event ) { - if ( element ) { - element = element.jquery || element.nodeType ? - $( element ) : - this.document.find( element ).eq( 0 ); - } + var draggable = $.ui.ddmanager.current; - if ( !element || !element[ 0 ] ) { - element = this.element.closest( ".ui-front, dialog" ); + // Bail if draggable and droppable are same element + if ( !draggable || ( draggable.currentItem || + draggable.element )[ 0 ] === this.element[ 0 ] ) { + return; } - if ( !element.length ) { - element = this.document[ 0 ].body; + if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || + draggable.element ) ) ) { + this._addHoverClass(); + this._trigger( "over", event, this.ui( draggable ) ); } - return element; }, - _toggleAttr: function() { - this.button.attr( "aria-expanded", this.isOpen ); - - // We can't use two _toggleClass() calls here, because we need to make sure - // we always remove classes first and add them second, otherwise if both classes have the - // same theme class, it will be removed after we add it. - this._removeClass( this.button, "ui-selectmenu-button-" + - ( this.isOpen ? "closed" : "open" ) ) - ._addClass( this.button, "ui-selectmenu-button-" + - ( this.isOpen ? "open" : "closed" ) ) - ._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen ); - - this.menu.attr( "aria-hidden", !this.isOpen ); - }, + _out: function( event ) { - _resizeButton: function() { - var width = this.options.width; + var draggable = $.ui.ddmanager.current; - // For `width: false`, just remove inline style and stop - if ( width === false ) { - this.button.css( "width", "" ); + // Bail if draggable and droppable are same element + if ( !draggable || ( draggable.currentItem || + draggable.element )[ 0 ] === this.element[ 0 ] ) { return; } - // For `width: null`, match the width of the original element - if ( width === null ) { - width = this.element.show().outerWidth(); - this.element.hide(); + if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || + draggable.element ) ) ) { + this._removeHoverClass(); + this._trigger( "out", event, this.ui( draggable ) ); } - this.button.outerWidth( width ); }, - _resizeMenu: function() { - this.menu.outerWidth( Math.max( - this.button.outerWidth(), + _drop: function( event, custom ) { - // Support: IE10 - // IE10 wraps long text (possibly a rounding bug) - // so we add 1px to avoid the wrapping - this.menu.width( "" ).outerWidth() + 1 - ) ); - }, + var draggable = custom || $.ui.ddmanager.current, + childrenIntersection = false; - _getCreateOptions: function() { - var options = this._super(); + // Bail if draggable and droppable are same element + if ( !draggable || ( draggable.currentItem || + draggable.element )[ 0 ] === this.element[ 0 ] ) { + return false; + } - options.disabled = this.element.prop( "disabled" ); + this.element + .find( ":data(ui-droppable)" ) + .not( ".ui-draggable-dragging" ) + .each( function() { + var inst = $( this ).droppable( "instance" ); + if ( + inst.options.greedy && + !inst.options.disabled && + inst.options.scope === draggable.options.scope && + inst.accept.call( + inst.element[ 0 ], ( draggable.currentItem || draggable.element ) + ) && + $.ui.intersect( + draggable, + $.extend( inst, { offset: inst.element.offset() } ), + inst.options.tolerance, event + ) + ) { + childrenIntersection = true; + return false; + } + } ); + if ( childrenIntersection ) { + return false; + } - return options; - }, + if ( this.accept.call( this.element[ 0 ], + ( draggable.currentItem || draggable.element ) ) ) { + this._removeActiveClass(); + this._removeHoverClass(); - _parseOptions: function( options ) { - var that = this, - data = []; - options.each( function( index, item ) { - if ( item.hidden ) { - return; - } + this._trigger( "drop", event, this.ui( draggable ) ); + return this.element; + } - data.push( that._parseOption( $( item ), index ) ); - } ); - this.items = data; - }, + return false; - _parseOption: function( option, index ) { - var optgroup = option.parent( "optgroup" ); + }, + ui: function( c ) { return { - element: option, - index: index, - value: option.val(), - label: option.text(), - optgroup: optgroup.attr( "label" ) || "", - disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" ) + draggable: ( c.currentItem || c.element ), + helper: c.helper, + position: c.position, + offset: c.positionAbs }; }, - _destroy: function() { - this._unbindFormResetHandler(); - this.menuWrap.remove(); - this.button.remove(); - this.element.show(); - this.element.removeUniqueId(); - this.labels.attr( "for", this.ids.element ); - } -} ] ); - - -/*! - * jQuery UI Slider 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Slider -//>>group: Widgets -//>>description: Displays a flexible slider with ranges and accessibility via keyboard. -//>>docs: http://api.jqueryui.com/slider/ -//>>demos: http://jqueryui.com/slider/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/slider.css -//>>css.theme: ../../themes/base/theme.css - - -var widgetsSlider = $.widget( "ui.slider", $.ui.mouse, { - version: "1.13.1", - widgetEventPrefix: "slide", - - options: { - animate: false, - classes: { - "ui-slider": "ui-corner-all", - "ui-slider-handle": "ui-corner-all", - - // Note: ui-widget-header isn't the most fittingly semantic framework class for this - // element, but worked best visually with a variety of themes - "ui-slider-range": "ui-corner-all ui-widget-header" - }, - distance: 0, - max: 100, - min: 0, - orientation: "horizontal", - range: false, - step: 1, - value: 0, - values: null, - - // Callbacks - change: null, - slide: null, - start: null, - stop: null + // Extension points just to make backcompat sane and avoid duplicating logic + // TODO: Remove in 1.14 along with call to it below + _addHoverClass: function() { + this._addClass( "ui-droppable-hover" ); }, - // Number of pages in a slider - // (how many times can you page up/down to go through the whole range) - numPages: 5, - - _create: function() { - this._keySliding = false; - this._mouseSliding = false; - this._animateOff = true; - this._handleIndex = null; - this._detectOrientation(); - this._mouseInit(); - this._calculateNewMax(); - - this._addClass( "ui-slider ui-slider-" + this.orientation, - "ui-widget ui-widget-content" ); - - this._refresh(); - - this._animateOff = false; + _removeHoverClass: function() { + this._removeClass( "ui-droppable-hover" ); }, - _refresh: function() { - this._createRange(); - this._createHandles(); - this._setupEvents(); - this._refreshValue(); + _addActiveClass: function() { + this._addClass( "ui-droppable-active" ); }, - _createHandles: function() { - var i, handleCount, - options = this.options, - existingHandles = this.element.find( ".ui-slider-handle" ), - handle = "<span tabindex='0'></span>", - handles = []; + _removeActiveClass: function() { + this._removeClass( "ui-droppable-active" ); + } +} ); - handleCount = ( options.values && options.values.length ) || 1; +$.ui.intersect = ( function() { + function isOverAxis( x, reference, size ) { + return ( x >= reference ) && ( x < ( reference + size ) ); + } - if ( existingHandles.length > handleCount ) { - existingHandles.slice( handleCount ).remove(); - existingHandles = existingHandles.slice( 0, handleCount ); - } + return function( draggable, droppable, toleranceMode, event ) { - for ( i = existingHandles.length; i < handleCount; i++ ) { - handles.push( handle ); + if ( !droppable.offset ) { + return false; } - this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); + var x1 = ( draggable.positionAbs || + draggable.position.absolute ).left + draggable.margins.left, + y1 = ( draggable.positionAbs || + draggable.position.absolute ).top + draggable.margins.top, + x2 = x1 + draggable.helperProportions.width, + y2 = y1 + draggable.helperProportions.height, + l = droppable.offset.left, + t = droppable.offset.top, + r = l + droppable.proportions().width, + b = t + droppable.proportions().height; - this._addClass( this.handles, "ui-slider-handle", "ui-state-default" ); + switch ( toleranceMode ) { + case "fit": + return ( l <= x1 && x2 <= r && t <= y1 && y2 <= b ); + case "intersect": + return ( l < x1 + ( draggable.helperProportions.width / 2 ) && // Right Half + x2 - ( draggable.helperProportions.width / 2 ) < r && // Left Half + t < y1 + ( draggable.helperProportions.height / 2 ) && // Bottom Half + y2 - ( draggable.helperProportions.height / 2 ) < b ); // Top Half + case "pointer": + return isOverAxis( event.pageY, t, droppable.proportions().height ) && + isOverAxis( event.pageX, l, droppable.proportions().width ); + case "touch": + return ( + ( y1 >= t && y1 <= b ) || // Top edge touching + ( y2 >= t && y2 <= b ) || // Bottom edge touching + ( y1 < t && y2 > b ) // Surrounded vertically + ) && ( + ( x1 >= l && x1 <= r ) || // Left edge touching + ( x2 >= l && x2 <= r ) || // Right edge touching + ( x1 < l && x2 > r ) // Surrounded horizontally + ); + default: + return false; + } + }; +} )(); - this.handle = this.handles.eq( 0 ); +/* + This manager tracks offsets of draggables and droppables +*/ +$.ui.ddmanager = { + current: null, + droppables: { "default": [] }, + prepareOffsets: function( t, event ) { - this.handles.each( function( i ) { - $( this ) - .data( "ui-slider-handle-index", i ) - .attr( "tabIndex", 0 ); - } ); - }, + var i, j, + m = $.ui.ddmanager.droppables[ t.options.scope ] || [], + type = event ? event.type : null, // workaround for #2317 + list = ( t.currentItem || t.element ).find( ":data(ui-droppable)" ).addBack(); - _createRange: function() { - var options = this.options; + droppablesLoop: for ( i = 0; i < m.length; i++ ) { - if ( options.range ) { - if ( options.range === true ) { - if ( !options.values ) { - options.values = [ this._valueMin(), this._valueMin() ]; - } else if ( options.values.length && options.values.length !== 2 ) { - options.values = [ options.values[ 0 ], options.values[ 0 ] ]; - } else if ( Array.isArray( options.values ) ) { - options.values = options.values.slice( 0 ); - } + // No disabled and non-accepted + if ( m[ i ].options.disabled || ( t && !m[ i ].accept.call( m[ i ].element[ 0 ], + ( t.currentItem || t.element ) ) ) ) { + continue; } - if ( !this.range || !this.range.length ) { - this.range = $( "<div>" ) - .appendTo( this.element ); - - this._addClass( this.range, "ui-slider-range" ); - } else { - this._removeClass( this.range, "ui-slider-range-min ui-slider-range-max" ); - - // Handle range switching from true to min/max - this.range.css( { - "left": "", - "bottom": "" - } ); + // Filter out elements in the current dragged item + for ( j = 0; j < list.length; j++ ) { + if ( list[ j ] === m[ i ].element[ 0 ] ) { + m[ i ].proportions().height = 0; + continue droppablesLoop; + } } - if ( options.range === "min" || options.range === "max" ) { - this._addClass( this.range, "ui-slider-range-" + options.range ); + + m[ i ].visible = m[ i ].element.css( "display" ) !== "none"; + if ( !m[ i ].visible ) { + continue; } - } else { - if ( this.range ) { - this.range.remove(); + + // Activate the droppable if used directly from draggables + if ( type === "mousedown" ) { + m[ i ]._activate.call( m[ i ], event ); } - this.range = null; - } - }, - _setupEvents: function() { - this._off( this.handles ); - this._on( this.handles, this._handleEvents ); - this._hoverable( this.handles ); - this._focusable( this.handles ); - }, + m[ i ].offset = m[ i ].element.offset(); + m[ i ].proportions( { + width: m[ i ].element[ 0 ].offsetWidth, + height: m[ i ].element[ 0 ].offsetHeight + } ); - _destroy: function() { - this.handles.remove(); - if ( this.range ) { - this.range.remove(); } - this._mouseDestroy(); }, + drop: function( draggable, event ) { - _mouseCapture: function( event ) { - var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, - that = this, - o = this.options; - - if ( o.disabled ) { - return false; - } + var dropped = false; - this.elementSize = { - width: this.element.outerWidth(), - height: this.element.outerHeight() - }; - this.elementOffset = this.element.offset(); + // Create a copy of the droppables in case the list changes during the drop (#9116) + $.each( ( $.ui.ddmanager.droppables[ draggable.options.scope ] || [] ).slice(), function() { - position = { x: event.pageX, y: event.pageY }; - normValue = this._normValueFromMouse( position ); - distance = this._valueMax() - this._valueMin() + 1; - this.handles.each( function( i ) { - var thisDistance = Math.abs( normValue - that.values( i ) ); - if ( ( distance > thisDistance ) || - ( distance === thisDistance && - ( i === that._lastChangedValue || that.values( i ) === o.min ) ) ) { - distance = thisDistance; - closestHandle = $( this ); - index = i; + if ( !this.options ) { + return; + } + if ( !this.options.disabled && this.visible && + $.ui.intersect( draggable, this, this.options.tolerance, event ) ) { + dropped = this._drop.call( this, event ) || dropped; } - } ); - allowed = this._start( event, index ); - if ( allowed === false ) { - return false; - } - this._mouseSliding = true; + if ( !this.options.disabled && this.visible && this.accept.call( this.element[ 0 ], + ( draggable.currentItem || draggable.element ) ) ) { + this.isout = true; + this.isover = false; + this._deactivate.call( this, event ); + } - this._handleIndex = index; + } ); + return dropped; - this._addClass( closestHandle, null, "ui-state-active" ); - closestHandle.trigger( "focus" ); + }, + dragStart: function( draggable, event ) { - offset = closestHandle.offset(); - mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); - this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { - left: event.pageX - offset.left - ( closestHandle.width() / 2 ), - top: event.pageY - offset.top - - ( closestHandle.height() / 2 ) - - ( parseInt( closestHandle.css( "borderTopWidth" ), 10 ) || 0 ) - - ( parseInt( closestHandle.css( "borderBottomWidth" ), 10 ) || 0 ) + - ( parseInt( closestHandle.css( "marginTop" ), 10 ) || 0 ) - }; + // Listen for scrolling so that if the dragging causes scrolling the position of the + // droppables can be recalculated (see #5003) + draggable.element.parentsUntil( "body" ).on( "scroll.droppable", function() { + if ( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + } ); + }, + drag: function( draggable, event ) { - if ( !this.handles.hasClass( "ui-state-hover" ) ) { - this._slide( event, index, normValue ); + // If you have a highly dynamic page, you might try this option. It renders positions + // every time you move the mouse. + if ( draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); } - this._animateOff = true; - return true; - }, - _mouseStart: function() { - return true; - }, + // Run through all droppables and check their positions based on specific tolerance options + $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { - _mouseDrag: function( event ) { - var position = { x: event.pageX, y: event.pageY }, - normValue = this._normValueFromMouse( position ); + if ( this.options.disabled || this.greedyChild || !this.visible ) { + return; + } + + var parentInstance, scope, parent, + intersects = $.ui.intersect( draggable, this, this.options.tolerance, event ), + c = !intersects && this.isover ? + "isout" : + ( intersects && !this.isover ? "isover" : null ); + if ( !c ) { + return; + } - this._slide( event, this._handleIndex, normValue ); + if ( this.options.greedy ) { - return false; - }, + // find droppable parents with same scope + scope = this.options.scope; + parent = this.element.parents( ":data(ui-droppable)" ).filter( function() { + return $( this ).droppable( "instance" ).options.scope === scope; + } ); - _mouseStop: function( event ) { - this._removeClass( this.handles, null, "ui-state-active" ); - this._mouseSliding = false; + if ( parent.length ) { + parentInstance = $( parent[ 0 ] ).droppable( "instance" ); + parentInstance.greedyChild = ( c === "isover" ); + } + } - this._stop( event, this._handleIndex ); - this._change( event, this._handleIndex ); + // We just moved into a greedy child + if ( parentInstance && c === "isover" ) { + parentInstance.isover = false; + parentInstance.isout = true; + parentInstance._out.call( parentInstance, event ); + } - this._handleIndex = null; - this._clickOffset = null; - this._animateOff = false; + this[ c ] = true; + this[ c === "isout" ? "isover" : "isout" ] = false; + this[ c === "isover" ? "_over" : "_out" ].call( this, event ); - return false; - }, + // We just moved out of a greedy child + if ( parentInstance && c === "isout" ) { + parentInstance.isout = false; + parentInstance.isover = true; + parentInstance._over.call( parentInstance, event ); + } + } ); - _detectOrientation: function() { - this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; }, + dragStop: function( draggable, event ) { + draggable.element.parentsUntil( "body" ).off( "scroll.droppable" ); - _normValueFromMouse: function( position ) { - var pixelTotal, - pixelMouse, - percentMouse, - valueTotal, - valueMouse; - - if ( this.orientation === "horizontal" ) { - pixelTotal = this.elementSize.width; - pixelMouse = position.x - this.elementOffset.left - - ( this._clickOffset ? this._clickOffset.left : 0 ); - } else { - pixelTotal = this.elementSize.height; - pixelMouse = position.y - this.elementOffset.top - - ( this._clickOffset ? this._clickOffset.top : 0 ); + // Call prepareOffsets one final time since IE does not fire return scroll events when + // overflow was caused by drag (see #5003) + if ( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); } + } +}; - percentMouse = ( pixelMouse / pixelTotal ); - if ( percentMouse > 1 ) { - percentMouse = 1; - } - if ( percentMouse < 0 ) { - percentMouse = 0; - } - if ( this.orientation === "vertical" ) { - percentMouse = 1 - percentMouse; +// DEPRECATED +// TODO: switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { + + // Backcompat for activeClass and hoverClass options + $.widget( "ui.droppable", $.ui.droppable, { + options: { + hoverClass: false, + activeClass: false + }, + _addActiveClass: function() { + this._super(); + if ( this.options.activeClass ) { + this.element.addClass( this.options.activeClass ); + } + }, + _removeActiveClass: function() { + this._super(); + if ( this.options.activeClass ) { + this.element.removeClass( this.options.activeClass ); + } + }, + _addHoverClass: function() { + this._super(); + if ( this.options.hoverClass ) { + this.element.addClass( this.options.hoverClass ); + } + }, + _removeHoverClass: function() { + this._super(); + if ( this.options.hoverClass ) { + this.element.removeClass( this.options.hoverClass ); + } } + } ); +} - valueTotal = this._valueMax() - this._valueMin(); - valueMouse = this._valueMin() + percentMouse * valueTotal; +var widgetsDroppable = $.ui.droppable; - return this._trimAlignValue( valueMouse ); - }, - _uiHash: function( index, value, values ) { - var uiHash = { - handle: this.handles[ index ], - handleIndex: index, - value: value !== undefined ? value : this.value() - }; +/*! + * jQuery UI Progressbar 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( this._hasMultipleValues() ) { - uiHash.value = value !== undefined ? value : this.values( index ); - uiHash.values = values || this.values(); - } +//>>label: Progressbar +//>>group: Widgets +/* eslint-disable max-len */ +//>>description: Displays a status indicator for loading state, standard percentage, and other progress indicators. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/progressbar/ +//>>demos: http://jqueryui.com/progressbar/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/progressbar.css +//>>css.theme: ../../themes/base/theme.css - return uiHash; - }, - _hasMultipleValues: function() { - return this.options.values && this.options.values.length; - }, +var widgetsProgressbar = $.widget( "ui.progressbar", { + version: "1.13.2", + options: { + classes: { + "ui-progressbar": "ui-corner-all", + "ui-progressbar-value": "ui-corner-left", + "ui-progressbar-complete": "ui-corner-right" + }, + max: 100, + value: 0, - _start: function( event, index ) { - return this._trigger( "start", event, this._uiHash( index ) ); + change: null, + complete: null }, - _slide: function( event, index, newVal ) { - var allowed, otherVal, - currentValue = this.value(), - newValues = this.values(); - - if ( this._hasMultipleValues() ) { - otherVal = this.values( index ? 0 : 1 ); - currentValue = this.values( index ); - - if ( this.options.values.length === 2 && this.options.range === true ) { - newVal = index === 0 ? Math.min( otherVal, newVal ) : Math.max( otherVal, newVal ); - } - - newValues[ index ] = newVal; - } + min: 0, - if ( newVal === currentValue ) { - return; - } + _create: function() { - allowed = this._trigger( "slide", event, this._uiHash( index, newVal, newValues ) ); + // Constrain initial value + this.oldValue = this.options.value = this._constrainedValue(); - // A slide can be canceled by returning false from the slide callback - if ( allowed === false ) { - return; - } + this.element.attr( { - if ( this._hasMultipleValues() ) { - this.values( index, newVal ); - } else { - this.value( newVal ); - } - }, + // Only set static values; aria-valuenow and aria-valuemax are + // set inside _refreshValue() + role: "progressbar", + "aria-valuemin": this.min + } ); + this._addClass( "ui-progressbar", "ui-widget ui-widget-content" ); - _stop: function( event, index ) { - this._trigger( "stop", event, this._uiHash( index ) ); + this.valueDiv = $( "<div>" ).appendTo( this.element ); + this._addClass( this.valueDiv, "ui-progressbar-value", "ui-widget-header" ); + this._refreshValue(); }, - _change: function( event, index ) { - if ( !this._keySliding && !this._mouseSliding ) { + _destroy: function() { + this.element.removeAttr( "role aria-valuemin aria-valuemax aria-valuenow" ); - //store the last changed value index for reference when handles overlap - this._lastChangedValue = index; - this._trigger( "change", event, this._uiHash( index ) ); - } + this.valueDiv.remove(); }, value: function( newValue ) { - if ( arguments.length ) { - this.options.value = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, 0 ); - return; - } - - return this._value(); - }, - - values: function( index, newValue ) { - var vals, - newValues, - i; - - if ( arguments.length > 1 ) { - this.options.values[ index ] = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, index ); - return; + if ( newValue === undefined ) { + return this.options.value; } - if ( arguments.length ) { - if ( Array.isArray( arguments[ 0 ] ) ) { - vals = this.options.values; - newValues = arguments[ 0 ]; - for ( i = 0; i < vals.length; i += 1 ) { - vals[ i ] = this._trimAlignValue( newValues[ i ] ); - this._change( null, i ); - } - this._refreshValue(); - } else { - if ( this._hasMultipleValues() ) { - return this._values( index ); - } else { - return this.value(); - } - } - } else { - return this._values(); - } + this.options.value = this._constrainedValue( newValue ); + this._refreshValue(); }, - _setOption: function( key, value ) { - var i, - valsLength = 0; - - if ( key === "range" && this.options.range === true ) { - if ( value === "min" ) { - this.options.value = this._values( 0 ); - this.options.values = null; - } else if ( value === "max" ) { - this.options.value = this._values( this.options.values.length - 1 ); - this.options.values = null; - } + _constrainedValue: function( newValue ) { + if ( newValue === undefined ) { + newValue = this.options.value; } - if ( Array.isArray( this.options.values ) ) { - valsLength = this.options.values.length; + this.indeterminate = newValue === false; + + // Sanitize value + if ( typeof newValue !== "number" ) { + newValue = 0; } - this._super( key, value ); + return this.indeterminate ? false : + Math.min( this.options.max, Math.max( this.min, newValue ) ); + }, - switch ( key ) { - case "orientation": - this._detectOrientation(); - this._removeClass( "ui-slider-horizontal ui-slider-vertical" ) - ._addClass( "ui-slider-" + this.orientation ); - this._refreshValue(); - if ( this.options.range ) { - this._refreshRange( value ); - } + _setOptions: function( options ) { - // Reset positioning from previous orientation - this.handles.css( value === "horizontal" ? "bottom" : "left", "" ); - break; - case "value": - this._animateOff = true; - this._refreshValue(); - this._change( null, 0 ); - this._animateOff = false; - break; - case "values": - this._animateOff = true; - this._refreshValue(); + // Ensure "value" option is set after other values (like max) + var value = options.value; + delete options.value; - // Start from the last handle to prevent unreachable handles (#9046) - for ( i = valsLength - 1; i >= 0; i-- ) { - this._change( null, i ); - } - this._animateOff = false; - break; - case "step": - case "min": - case "max": - this._animateOff = true; - this._calculateNewMax(); - this._refreshValue(); - this._animateOff = false; - break; - case "range": - this._animateOff = true; - this._refresh(); - this._animateOff = false; - break; + this._super( options ); + + this.options.value = this._constrainedValue( value ); + this._refreshValue(); + }, + + _setOption: function( key, value ) { + if ( key === "max" ) { + + // Don't allow a max less than min + value = Math.max( this.min, value ); } + this._super( key, value ); }, _setOptionDisabled: function( value ) { this._super( value ); + this.element.attr( "aria-disabled", value ); this._toggleClass( null, "ui-state-disabled", !!value ); }, - //internal value getter - // _value() returns value trimmed by min and max, aligned by step - _value: function() { - var val = this.options.value; - val = this._trimAlignValue( val ); - - return val; + _percentage: function() { + return this.indeterminate ? + 100 : + 100 * ( this.options.value - this.min ) / ( this.options.max - this.min ); }, - //internal values getter - // _values() returns array of values trimmed by min and max, aligned by step - // _values( index ) returns single value trimmed by min and max, aligned by step - _values: function( index ) { - var val, - vals, - i; + _refreshValue: function() { + var value = this.options.value, + percentage = this._percentage(); - if ( arguments.length ) { - val = this.options.values[ index ]; - val = this._trimAlignValue( val ); + this.valueDiv + .toggle( this.indeterminate || value > this.min ) + .width( percentage.toFixed( 0 ) + "%" ); - return val; - } else if ( this._hasMultipleValues() ) { + this + ._toggleClass( this.valueDiv, "ui-progressbar-complete", null, + value === this.options.max ) + ._toggleClass( "ui-progressbar-indeterminate", null, this.indeterminate ); - // .slice() creates a copy of the array - // this copy gets trimmed by min and max and then returned - vals = this.options.values.slice(); - for ( i = 0; i < vals.length; i += 1 ) { - vals[ i ] = this._trimAlignValue( vals[ i ] ); + if ( this.indeterminate ) { + this.element.removeAttr( "aria-valuenow" ); + if ( !this.overlayDiv ) { + this.overlayDiv = $( "<div>" ).appendTo( this.valueDiv ); + this._addClass( this.overlayDiv, "ui-progressbar-overlay" ); } - - return vals; } else { - return []; + this.element.attr( { + "aria-valuemax": this.options.max, + "aria-valuenow": value + } ); + if ( this.overlayDiv ) { + this.overlayDiv.remove(); + this.overlayDiv = null; + } } - }, - // Returns the step-aligned value that val is closest to, between (inclusive) min and max - _trimAlignValue: function( val ) { - if ( val <= this._valueMin() ) { - return this._valueMin(); + if ( this.oldValue !== value ) { + this.oldValue = value; + this._trigger( "change" ); } - if ( val >= this._valueMax() ) { - return this._valueMax(); + if ( value === this.options.max ) { + this._trigger( "complete" ); } - var step = ( this.options.step > 0 ) ? this.options.step : 1, - valModStep = ( val - this._valueMin() ) % step, - alignValue = val - valModStep; + } +} ); + + +/*! + * jQuery UI Selectable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Selectable +//>>group: Interactions +//>>description: Allows groups of elements to be selected with the mouse. +//>>docs: http://api.jqueryui.com/selectable/ +//>>demos: http://jqueryui.com/selectable/ +//>>css.structure: ../../themes/base/selectable.css + + +var widgetsSelectable = $.widget( "ui.selectable", $.ui.mouse, { + version: "1.13.2", + options: { + appendTo: "body", + autoRefresh: true, + distance: 0, + filter: "*", + tolerance: "touch", + + // Callbacks + selected: null, + selecting: null, + start: null, + stop: null, + unselected: null, + unselecting: null + }, + _create: function() { + var that = this; + + this._addClass( "ui-selectable" ); + + this.dragged = false; + + // Cache selectee children based on filter + this.refresh = function() { + that.elementPos = $( that.element[ 0 ] ).offset(); + that.selectees = $( that.options.filter, that.element[ 0 ] ); + that._addClass( that.selectees, "ui-selectee" ); + that.selectees.each( function() { + var $this = $( this ), + selecteeOffset = $this.offset(), + pos = { + left: selecteeOffset.left - that.elementPos.left, + top: selecteeOffset.top - that.elementPos.top + }; + $.data( this, "selectable-item", { + element: this, + $element: $this, + left: pos.left, + top: pos.top, + right: pos.left + $this.outerWidth(), + bottom: pos.top + $this.outerHeight(), + startselected: false, + selected: $this.hasClass( "ui-selected" ), + selecting: $this.hasClass( "ui-selecting" ), + unselecting: $this.hasClass( "ui-unselecting" ) + } ); + } ); + }; + this.refresh(); + + this._mouseInit(); + + this.helper = $( "<div>" ); + this._addClass( this.helper, "ui-selectable-helper" ); + }, + + _destroy: function() { + this.selectees.removeData( "selectable-item" ); + this._mouseDestroy(); + }, + + _mouseStart: function( event ) { + var that = this, + options = this.options; + + this.opos = [ event.pageX, event.pageY ]; + this.elementPos = $( this.element[ 0 ] ).offset(); + + if ( this.options.disabled ) { + return; + } + + this.selectees = $( options.filter, this.element[ 0 ] ); + + this._trigger( "start", event ); + + $( options.appendTo ).append( this.helper ); + + // position helper (lasso) + this.helper.css( { + "left": event.pageX, + "top": event.pageY, + "width": 0, + "height": 0 + } ); - if ( Math.abs( valModStep ) * 2 >= step ) { - alignValue += ( valModStep > 0 ) ? step : ( -step ); + if ( options.autoRefresh ) { + this.refresh(); } - // Since JavaScript has problems with large floats, round - // the final value to 5 digits after the decimal point (see #4124) - return parseFloat( alignValue.toFixed( 5 ) ); - }, + this.selectees.filter( ".ui-selected" ).each( function() { + var selectee = $.data( this, "selectable-item" ); + selectee.startselected = true; + if ( !event.metaKey && !event.ctrlKey ) { + that._removeClass( selectee.$element, "ui-selected" ); + selectee.selected = false; + that._addClass( selectee.$element, "ui-unselecting" ); + selectee.unselecting = true; - _calculateNewMax: function() { - var max = this.options.max, - min = this._valueMin(), - step = this.options.step, - aboveMin = Math.round( ( max - min ) / step ) * step; - max = aboveMin + min; - if ( max > this.options.max ) { + // selectable UNSELECTING callback + that._trigger( "unselecting", event, { + unselecting: selectee.element + } ); + } + } ); - //If max is not divisible by step, rounding off may increase its value - max -= step; - } - this.max = parseFloat( max.toFixed( this._precision() ) ); - }, + $( event.target ).parents().addBack().each( function() { + var doSelect, + selectee = $.data( this, "selectable-item" ); + if ( selectee ) { + doSelect = ( !event.metaKey && !event.ctrlKey ) || + !selectee.$element.hasClass( "ui-selected" ); + that._removeClass( selectee.$element, doSelect ? "ui-unselecting" : "ui-selected" ) + ._addClass( selectee.$element, doSelect ? "ui-selecting" : "ui-unselecting" ); + selectee.unselecting = !doSelect; + selectee.selecting = doSelect; + selectee.selected = doSelect; - _precision: function() { - var precision = this._precisionOf( this.options.step ); - if ( this.options.min !== null ) { - precision = Math.max( precision, this._precisionOf( this.options.min ) ); - } - return precision; - }, + // selectable (UN)SELECTING callback + if ( doSelect ) { + that._trigger( "selecting", event, { + selecting: selectee.element + } ); + } else { + that._trigger( "unselecting", event, { + unselecting: selectee.element + } ); + } + return false; + } + } ); - _precisionOf: function( num ) { - var str = num.toString(), - decimal = str.indexOf( "." ); - return decimal === -1 ? 0 : str.length - decimal - 1; }, - _valueMin: function() { - return this.options.min; - }, + _mouseDrag: function( event ) { - _valueMax: function() { - return this.max; - }, + this.dragged = true; - _refreshRange: function( orientation ) { - if ( orientation === "vertical" ) { - this.range.css( { "width": "", "left": "" } ); - } - if ( orientation === "horizontal" ) { - this.range.css( { "height": "", "bottom": "" } ); + if ( this.options.disabled ) { + return; } - }, - _refreshValue: function() { - var lastValPercent, valPercent, value, valueMin, valueMax, - oRange = this.options.range, - o = this.options, + var tmp, that = this, - animate = ( !this._animateOff ) ? o.animate : false, - _set = {}; + options = this.options, + x1 = this.opos[ 0 ], + y1 = this.opos[ 1 ], + x2 = event.pageX, + y2 = event.pageY; - if ( this._hasMultipleValues() ) { - this.handles.each( function( i ) { - valPercent = ( that.values( i ) - that._valueMin() ) / ( that._valueMax() - - that._valueMin() ) * 100; - _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - if ( that.options.range === true ) { - if ( that.orientation === "horizontal" ) { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - left: valPercent + "%" - }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { - width: ( valPercent - lastValPercent ) + "%" - }, { - queue: false, - duration: o.animate - } ); - } - } else { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - bottom: ( valPercent ) + "%" - }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { - height: ( valPercent - lastValPercent ) + "%" - }, { - queue: false, - duration: o.animate - } ); - } - } - } - lastValPercent = valPercent; - } ); - } else { - value = this.value(); - valueMin = this._valueMin(); - valueMax = this._valueMax(); - valPercent = ( valueMax !== valueMin ) ? - ( value - valueMin ) / ( valueMax - valueMin ) * 100 : - 0; - _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( x1 > x2 ) { + tmp = x2; x2 = x1; x1 = tmp; + } + if ( y1 > y2 ) { + tmp = y2; y2 = y1; y1 = tmp; + } + this.helper.css( { left: x1, top: y1, width: x2 - x1, height: y2 - y1 } ); - if ( oRange === "min" && this.orientation === "horizontal" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - width: valPercent + "%" - }, o.animate ); - } - if ( oRange === "max" && this.orientation === "horizontal" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - width: ( 100 - valPercent ) + "%" - }, o.animate ); - } - if ( oRange === "min" && this.orientation === "vertical" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - height: valPercent + "%" - }, o.animate ); - } - if ( oRange === "max" && this.orientation === "vertical" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { - height: ( 100 - valPercent ) + "%" - }, o.animate ); + this.selectees.each( function() { + var selectee = $.data( this, "selectable-item" ), + hit = false, + offset = {}; + + //prevent helper from being selected if appendTo: selectable + if ( !selectee || selectee.element === that.element[ 0 ] ) { + return; } - } - }, - _handleEvents: { - keydown: function( event ) { - var allowed, curVal, newVal, step, - index = $( event.target ).data( "ui-slider-handle-index" ); + offset.left = selectee.left + that.elementPos.left; + offset.right = selectee.right + that.elementPos.left; + offset.top = selectee.top + that.elementPos.top; + offset.bottom = selectee.bottom + that.elementPos.top; - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - case $.ui.keyCode.END: - case $.ui.keyCode.PAGE_UP: - case $.ui.keyCode.PAGE_DOWN: - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - event.preventDefault(); - if ( !this._keySliding ) { - this._keySliding = true; - this._addClass( $( event.target ), null, "ui-state-active" ); - allowed = this._start( event, index ); - if ( allowed === false ) { - return; - } - } - break; + if ( options.tolerance === "touch" ) { + hit = ( !( offset.left > x2 || offset.right < x1 || offset.top > y2 || + offset.bottom < y1 ) ); + } else if ( options.tolerance === "fit" ) { + hit = ( offset.left > x1 && offset.right < x2 && offset.top > y1 && + offset.bottom < y2 ); } - step = this.options.step; - if ( this._hasMultipleValues() ) { - curVal = newVal = this.values( index ); + if ( hit ) { + + // SELECT + if ( selectee.selected ) { + that._removeClass( selectee.$element, "ui-selected" ); + selectee.selected = false; + } + if ( selectee.unselecting ) { + that._removeClass( selectee.$element, "ui-unselecting" ); + selectee.unselecting = false; + } + if ( !selectee.selecting ) { + that._addClass( selectee.$element, "ui-selecting" ); + selectee.selecting = true; + + // selectable SELECTING callback + that._trigger( "selecting", event, { + selecting: selectee.element + } ); + } } else { - curVal = newVal = this.value(); - } - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - newVal = this._valueMin(); - break; - case $.ui.keyCode.END: - newVal = this._valueMax(); - break; - case $.ui.keyCode.PAGE_UP: - newVal = this._trimAlignValue( - curVal + ( ( this._valueMax() - this._valueMin() ) / this.numPages ) - ); - break; - case $.ui.keyCode.PAGE_DOWN: - newVal = this._trimAlignValue( - curVal - ( ( this._valueMax() - this._valueMin() ) / this.numPages ) ); - break; - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - if ( curVal === this._valueMax() ) { - return; + // UNSELECT + if ( selectee.selecting ) { + if ( ( event.metaKey || event.ctrlKey ) && selectee.startselected ) { + that._removeClass( selectee.$element, "ui-selecting" ); + selectee.selecting = false; + that._addClass( selectee.$element, "ui-selected" ); + selectee.selected = true; + } else { + that._removeClass( selectee.$element, "ui-selecting" ); + selectee.selecting = false; + if ( selectee.startselected ) { + that._addClass( selectee.$element, "ui-unselecting" ); + selectee.unselecting = true; + } + + // selectable UNSELECTING callback + that._trigger( "unselecting", event, { + unselecting: selectee.element + } ); } - newVal = this._trimAlignValue( curVal + step ); - break; - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - if ( curVal === this._valueMin() ) { - return; + } + if ( selectee.selected ) { + if ( !event.metaKey && !event.ctrlKey && !selectee.startselected ) { + that._removeClass( selectee.$element, "ui-selected" ); + selectee.selected = false; + + that._addClass( selectee.$element, "ui-unselecting" ); + selectee.unselecting = true; + + // selectable UNSELECTING callback + that._trigger( "unselecting", event, { + unselecting: selectee.element + } ); } - newVal = this._trimAlignValue( curVal - step ); - break; + } } + } ); - this._slide( event, index, newVal ); - }, - keyup: function( event ) { - var index = $( event.target ).data( "ui-slider-handle-index" ); + return false; + }, - if ( this._keySliding ) { - this._keySliding = false; - this._stop( event, index ); - this._change( event, index ); - this._removeClass( $( event.target ), null, "ui-state-active" ); - } - } + _mouseStop: function( event ) { + var that = this; + + this.dragged = false; + + $( ".ui-unselecting", this.element[ 0 ] ).each( function() { + var selectee = $.data( this, "selectable-item" ); + that._removeClass( selectee.$element, "ui-unselecting" ); + selectee.unselecting = false; + selectee.startselected = false; + that._trigger( "unselected", event, { + unselected: selectee.element + } ); + } ); + $( ".ui-selecting", this.element[ 0 ] ).each( function() { + var selectee = $.data( this, "selectable-item" ); + that._removeClass( selectee.$element, "ui-selecting" ) + ._addClass( selectee.$element, "ui-selected" ); + selectee.selecting = false; + selectee.selected = true; + selectee.startselected = true; + that._trigger( "selected", event, { + selected: selectee.element + } ); + } ); + this._trigger( "stop", event ); + + this.helper.remove(); + + return false; } + } ); /*! - * jQuery UI Spinner 1.13.1 + * jQuery UI Selectmenu 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -14482,560 +14098,668 @@ var widgetsSlider = $.widget( "ui.slider", $.ui.mouse, { * http://jquery.org/license */ -//>>label: Spinner +//>>label: Selectmenu //>>group: Widgets -//>>description: Displays buttons to easily input numbers via the keyboard or mouse. -//>>docs: http://api.jqueryui.com/spinner/ -//>>demos: http://jqueryui.com/spinner/ +/* eslint-disable max-len */ +//>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/selectmenu/ +//>>demos: http://jqueryui.com/selectmenu/ //>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/spinner.css +//>>css.structure: ../../themes/base/selectmenu.css, ../../themes/base/button.css //>>css.theme: ../../themes/base/theme.css -function spinnerModifier( fn ) { - return function() { - var previous = this.element.val(); - fn.apply( this, arguments ); - this._refresh(); - if ( previous !== this.element.val() ) { - this._trigger( "change" ); - } - }; -} +var widgetsSelectmenu = $.widget( "ui.selectmenu", [ $.ui.formResetMixin, { + version: "1.13.2", + defaultElement: "<select>", + options: { + appendTo: null, + classes: { + "ui-selectmenu-button-open": "ui-corner-top", + "ui-selectmenu-button-closed": "ui-corner-all" + }, + disabled: null, + icons: { + button: "ui-icon-triangle-1-s" + }, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + width: false, + + // Callbacks + change: null, + close: null, + focus: null, + open: null, + select: null + }, + + _create: function() { + var selectmenuId = this.element.uniqueId().attr( "id" ); + this.ids = { + element: selectmenuId, + button: selectmenuId + "-button", + menu: selectmenuId + "-menu" + }; + + this._drawButton(); + this._drawMenu(); + this._bindFormResetHandler(); + + this._rendered = false; + this.menuItems = $(); + }, + + _drawButton: function() { + var icon, + that = this, + item = this._parseOption( + this.element.find( "option:selected" ), + this.element[ 0 ].selectedIndex + ); + + // Associate existing label with the new button + this.labels = this.element.labels().attr( "for", this.ids.button ); + this._on( this.labels, { + click: function( event ) { + this.button.trigger( "focus" ); + event.preventDefault(); + } + } ); + + // Hide original select element + this.element.hide(); + + // Create button + this.button = $( "<span>", { + tabindex: this.options.disabled ? -1 : 0, + id: this.ids.button, + role: "combobox", + "aria-expanded": "false", + "aria-autocomplete": "list", + "aria-owns": this.ids.menu, + "aria-haspopup": "true", + title: this.element.attr( "title" ) + } ) + .insertAfter( this.element ); + + this._addClass( this.button, "ui-selectmenu-button ui-selectmenu-button-closed", + "ui-button ui-widget" ); + + icon = $( "<span>" ).appendTo( this.button ); + this._addClass( icon, "ui-selectmenu-icon", "ui-icon " + this.options.icons.button ); + this.buttonItem = this._renderButtonItem( item ) + .appendTo( this.button ); + + if ( this.options.width !== false ) { + this._resizeButton(); + } + + this._on( this.button, this._buttonEvents ); + this.button.one( "focusin", function() { + + // Delay rendering the menu items until the button receives focus. + // The menu may have already been rendered via a programmatic open. + if ( !that._rendered ) { + that._refreshMenu(); + } + } ); + }, + + _drawMenu: function() { + var that = this; + + // Create menu + this.menu = $( "<ul>", { + "aria-hidden": "true", + "aria-labelledby": this.ids.button, + id: this.ids.menu + } ); + + // Wrap menu + this.menuWrap = $( "<div>" ).append( this.menu ); + this._addClass( this.menuWrap, "ui-selectmenu-menu", "ui-front" ); + this.menuWrap.appendTo( this._appendTo() ); + + // Initialize menu widget + this.menuInstance = this.menu + .menu( { + classes: { + "ui-menu": "ui-corner-bottom" + }, + role: "listbox", + select: function( event, ui ) { + event.preventDefault(); + + // Support: IE8 + // If the item was selected via a click, the text selection + // will be destroyed in IE + that._setSelection(); + + that._select( ui.item.data( "ui-selectmenu-item" ), event ); + }, + focus: function( event, ui ) { + var item = ui.item.data( "ui-selectmenu-item" ); + + // Prevent inital focus from firing and check if its a newly focused item + if ( that.focusIndex != null && item.index !== that.focusIndex ) { + that._trigger( "focus", event, { item: item } ); + if ( !that.isOpen ) { + that._select( item, event ); + } + } + that.focusIndex = item.index; + + that.button.attr( "aria-activedescendant", + that.menuItems.eq( item.index ).attr( "id" ) ); + } + } ) + .menu( "instance" ); + + // Don't close the menu on mouseleave + this.menuInstance._off( this.menu, "mouseleave" ); + + // Cancel the menu's collapseAll on document click + this.menuInstance._closeOnDocumentClick = function() { + return false; + }; + + // Selects often contain empty items, but never contain dividers + this.menuInstance._isDivider = function() { + return false; + }; + }, -$.widget( "ui.spinner", { - version: "1.13.1", - defaultElement: "<input>", - widgetEventPrefix: "spin", - options: { - classes: { - "ui-spinner": "ui-corner-all", - "ui-spinner-down": "ui-corner-br", - "ui-spinner-up": "ui-corner-tr" - }, - culture: null, - icons: { - down: "ui-icon-triangle-1-s", - up: "ui-icon-triangle-1-n" - }, - incremental: true, - max: null, - min: null, - numberFormat: null, - page: 10, - step: 1, + refresh: function() { + this._refreshMenu(); + this.buttonItem.replaceWith( + this.buttonItem = this._renderButtonItem( - change: null, - spin: null, - start: null, - stop: null + // Fall back to an empty object in case there are no options + this._getSelectedItem().data( "ui-selectmenu-item" ) || {} + ) + ); + if ( this.options.width === null ) { + this._resizeButton(); + } }, - _create: function() { + _refreshMenu: function() { + var item, + options = this.element.find( "option" ); - // handle string values that need to be parsed - this._setOption( "max", this.options.max ); - this._setOption( "min", this.options.min ); - this._setOption( "step", this.options.step ); + this.menu.empty(); - // Only format if there is a value, prevents the field from being marked - // as invalid in Firefox, see #9573. - if ( this.value() !== "" ) { + this._parseOptions( options ); + this._renderMenu( this.menu, this.items ); - // Format the value, but don't constrain. - this._value( this.element.val(), true ); - } + this.menuInstance.refresh(); + this.menuItems = this.menu.find( "li" ) + .not( ".ui-selectmenu-optgroup" ) + .find( ".ui-menu-item-wrapper" ); - this._draw(); - this._on( this._events ); - this._refresh(); + this._rendered = true; - // Turning off autocomplete prevents the browser from remembering the - // value when navigating through history, so we re-enable autocomplete - // if the page is unloaded before the widget is destroyed. #7790 - this._on( this.window, { - beforeunload: function() { - this.element.removeAttr( "autocomplete" ); - } - } ); - }, + if ( !options.length ) { + return; + } - _getCreateOptions: function() { - var options = this._super(); - var element = this.element; + item = this._getSelectedItem(); - $.each( [ "min", "max", "step" ], function( i, option ) { - var value = element.attr( option ); - if ( value != null && value.length ) { - options[ option ] = value; - } - } ); + // Update the menu to have the correct item focused + this.menuInstance.focus( null, item ); + this._setAria( item.data( "ui-selectmenu-item" ) ); - return options; + // Set disabled state + this._setOption( "disabled", this.element.prop( "disabled" ) ); }, - _events: { - keydown: function( event ) { - if ( this._start( event ) && this._keydown( event ) ) { - event.preventDefault(); - } - }, - keyup: "_stop", - focus: function() { - this.previous = this.element.val(); - }, - blur: function( event ) { - if ( this.cancelBlur ) { - delete this.cancelBlur; - return; - } + open: function( event ) { + if ( this.options.disabled ) { + return; + } - this._stop(); - this._refresh(); - if ( this.previous !== this.element.val() ) { - this._trigger( "change", event ); - } - }, - mousewheel: function( event, delta ) { - var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ); - var isActive = this.element[ 0 ] === activeElement; + // If this is the first time the menu is being opened, render the items + if ( !this._rendered ) { + this._refreshMenu(); + } else { - if ( !isActive || !delta ) { - return; - } + // Menu clears focus on close, reset focus to selected item + this._removeClass( this.menu.find( ".ui-state-active" ), null, "ui-state-active" ); + this.menuInstance.focus( null, this._getSelectedItem() ); + } - if ( !this.spinning && !this._start( event ) ) { - return false; - } + // If there are no options, don't open the menu + if ( !this.menuItems.length ) { + return; + } - this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event ); - clearTimeout( this.mousewheelTimer ); - this.mousewheelTimer = this._delay( function() { - if ( this.spinning ) { - this._stop( event ); - } - }, 100 ); - event.preventDefault(); - }, - "mousedown .ui-spinner-button": function( event ) { - var previous; + this.isOpen = true; + this._toggleAttr(); + this._resizeMenu(); + this._position(); - // We never want the buttons to have focus; whenever the user is - // interacting with the spinner, the focus should be on the input. - // If the input is focused then this.previous is properly set from - // when the input first received focus. If the input is not focused - // then we need to set this.previous based on the value before spinning. - previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ? - this.previous : this.element.val(); - function checkFocus() { - var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ); - if ( !isActive ) { - this.element.trigger( "focus" ); - this.previous = previous; + this._on( this.document, this._documentClick ); - // support: IE - // IE sets focus asynchronously, so we need to check if focus - // moved off of the input because the user clicked on the button. - this._delay( function() { - this.previous = previous; - } ); - } - } + this._trigger( "open", event ); + }, - // Ensure focus is on (or stays on) the text field - event.preventDefault(); - checkFocus.call( this ); + _position: function() { + this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) ); + }, - // Support: IE - // IE doesn't prevent moving focus even with event.preventDefault() - // so we set a flag to know when we should ignore the blur event - // and check (again) if focus moved off of the input. - this.cancelBlur = true; - this._delay( function() { - delete this.cancelBlur; - checkFocus.call( this ); - } ); + close: function( event ) { + if ( !this.isOpen ) { + return; + } - if ( this._start( event ) === false ) { - return; - } + this.isOpen = false; + this._toggleAttr(); - this._repeat( null, $( event.currentTarget ) - .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); - }, - "mouseup .ui-spinner-button": "_stop", - "mouseenter .ui-spinner-button": function( event ) { + this.range = null; + this._off( this.document ); - // button will add ui-state-active if mouse was down while mouseleave and kept down - if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { - return; - } + this._trigger( "close", event ); + }, - if ( this._start( event ) === false ) { - return false; - } - this._repeat( null, $( event.currentTarget ) - .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); - }, + widget: function() { + return this.button; + }, - // TODO: do we really want to consider this a stop? - // shouldn't we just stop the repeater and wait until mouseup before - // we trigger the stop event? - "mouseleave .ui-spinner-button": "_stop" + menuWidget: function() { + return this.menu; }, - // Support mobile enhanced option and make backcompat more sane - _enhance: function() { - this.uiSpinner = this.element - .attr( "autocomplete", "off" ) - .wrap( "<span>" ) - .parent() + _renderButtonItem: function( item ) { + var buttonItem = $( "<span>" ); + + this._setText( buttonItem, item.label ); + this._addClass( buttonItem, "ui-selectmenu-text" ); - // Add buttons - .append( - "<a></a><a></a>" - ); + return buttonItem; }, - _draw: function() { - this._enhance(); + _renderMenu: function( ul, items ) { + var that = this, + currentOptgroup = ""; - this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" ); - this._addClass( "ui-spinner-input" ); + $.each( items, function( index, item ) { + var li; - this.element.attr( "role", "spinbutton" ); + if ( item.optgroup !== currentOptgroup ) { + li = $( "<li>", { + text: item.optgroup + } ); + that._addClass( li, "ui-selectmenu-optgroup", "ui-menu-divider" + + ( item.element.parent( "optgroup" ).prop( "disabled" ) ? + " ui-state-disabled" : + "" ) ); - // Button bindings - this.buttons = this.uiSpinner.children( "a" ) - .attr( "tabIndex", -1 ) - .attr( "aria-hidden", true ) - .button( { - classes: { - "ui-button": "" - } - } ); + li.appendTo( ul ); - // TODO: Right now button does not support classes this is already updated in button PR - this._removeClass( this.buttons, "ui-corner-all" ); + currentOptgroup = item.optgroup; + } - this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" ); - this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" ); - this.buttons.first().button( { - "icon": this.options.icons.up, - "showLabel": false - } ); - this.buttons.last().button( { - "icon": this.options.icons.down, - "showLabel": false + that._renderItemData( ul, item ); } ); + }, - // IE 6 doesn't understand height: 50% for the buttons - // unless the wrapper has an explicit height - if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) && - this.uiSpinner.height() > 0 ) { - this.uiSpinner.height( this.uiSpinner.height() ); - } + _renderItemData: function( ul, item ) { + return this._renderItem( ul, item ).data( "ui-selectmenu-item", item ); }, - _keydown: function( event ) { - var options = this.options, - keyCode = $.ui.keyCode; + _renderItem: function( ul, item ) { + var li = $( "<li>" ), + wrapper = $( "<div>", { + title: item.element.attr( "title" ) + } ); - switch ( event.keyCode ) { - case keyCode.UP: - this._repeat( null, 1, event ); - return true; - case keyCode.DOWN: - this._repeat( null, -1, event ); - return true; - case keyCode.PAGE_UP: - this._repeat( null, options.page, event ); - return true; - case keyCode.PAGE_DOWN: - this._repeat( null, -options.page, event ); - return true; + if ( item.disabled ) { + this._addClass( li, null, "ui-state-disabled" ); } + this._setText( wrapper, item.label ); - return false; + return li.append( wrapper ).appendTo( ul ); }, - _start: function( event ) { - if ( !this.spinning && this._trigger( "start", event ) === false ) { - return false; - } - - if ( !this.counter ) { - this.counter = 1; + _setText: function( element, value ) { + if ( value ) { + element.text( value ); + } else { + element.html( " " ); } - this.spinning = true; - return true; }, - _repeat: function( i, steps, event ) { - i = i || 500; + _move: function( direction, event ) { + var item, next, + filter = ".ui-menu-item"; - clearTimeout( this.timer ); - this.timer = this._delay( function() { - this._repeat( 40, steps, event ); - }, i ); + if ( this.isOpen ) { + item = this.menuItems.eq( this.focusIndex ).parent( "li" ); + } else { + item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" ); + filter += ":not(.ui-state-disabled)"; + } - this._spin( steps * this.options.step, event ); + if ( direction === "first" || direction === "last" ) { + next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 ); + } else { + next = item[ direction + "All" ]( filter ).eq( 0 ); + } + + if ( next.length ) { + this.menuInstance.focus( event, next ); + } }, - _spin: function( step, event ) { - var value = this.value() || 0; + _getSelectedItem: function() { + return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" ); + }, - if ( !this.counter ) { - this.counter = 1; - } + _toggle: function( event ) { + this[ this.isOpen ? "close" : "open" ]( event ); + }, - value = this._adjustValue( value + step * this._increment( this.counter ) ); + _setSelection: function() { + var selection; - if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) { - this._value( value ); - this.counter++; + if ( !this.range ) { + return; } - }, - _increment: function( i ) { - var incremental = this.options.incremental; + if ( window.getSelection ) { + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange( this.range ); - if ( incremental ) { - return typeof incremental === "function" ? - incremental( i ) : - Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 ); + // Support: IE8 + } else { + this.range.select(); } - return 1; + // Support: IE + // Setting the text selection kills the button focus in IE, but + // restoring the focus doesn't kill the selection. + this.button.trigger( "focus" ); }, - _precision: function() { - var precision = this._precisionOf( this.options.step ); - if ( this.options.min !== null ) { - precision = Math.max( precision, this._precisionOf( this.options.min ) ); + _documentClick: { + mousedown: function( event ) { + if ( !this.isOpen ) { + return; + } + + if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" + + $.escapeSelector( this.ids.button ) ).length ) { + this.close( event ); + } } - return precision; }, - _precisionOf: function( num ) { - var str = num.toString(), - decimal = str.indexOf( "." ); - return decimal === -1 ? 0 : str.length - decimal - 1; - }, + _buttonEvents: { - _adjustValue: function( value ) { - var base, aboveMin, - options = this.options; + // Prevent text selection from being reset when interacting with the selectmenu (#10144) + mousedown: function() { + var selection; - // Make sure we're at a valid step - // - find out where we are relative to the base (min or 0) - base = options.min !== null ? options.min : 0; - aboveMin = value - base; + if ( window.getSelection ) { + selection = window.getSelection(); + if ( selection.rangeCount ) { + this.range = selection.getRangeAt( 0 ); + } + + // Support: IE8 + } else { + this.range = document.selection.createRange(); + } + }, + + click: function( event ) { + this._setSelection(); + this._toggle( event ); + }, + + keydown: function( event ) { + var preventDefault = true; + switch ( event.keyCode ) { + case $.ui.keyCode.TAB: + case $.ui.keyCode.ESCAPE: + this.close( event ); + preventDefault = false; + break; + case $.ui.keyCode.ENTER: + if ( this.isOpen ) { + this._selectFocusedItem( event ); + } + break; + case $.ui.keyCode.UP: + if ( event.altKey ) { + this._toggle( event ); + } else { + this._move( "prev", event ); + } + break; + case $.ui.keyCode.DOWN: + if ( event.altKey ) { + this._toggle( event ); + } else { + this._move( "next", event ); + } + break; + case $.ui.keyCode.SPACE: + if ( this.isOpen ) { + this._selectFocusedItem( event ); + } else { + this._toggle( event ); + } + break; + case $.ui.keyCode.LEFT: + this._move( "prev", event ); + break; + case $.ui.keyCode.RIGHT: + this._move( "next", event ); + break; + case $.ui.keyCode.HOME: + case $.ui.keyCode.PAGE_UP: + this._move( "first", event ); + break; + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_DOWN: + this._move( "last", event ); + break; + default: + this.menu.trigger( event ); + preventDefault = false; + } - // - round to the nearest step - aboveMin = Math.round( aboveMin / options.step ) * options.step; + if ( preventDefault ) { + event.preventDefault(); + } + } + }, - // - rounding is based on 0, so adjust back to our base - value = base + aboveMin; + _selectFocusedItem: function( event ) { + var item = this.menuItems.eq( this.focusIndex ).parent( "li" ); + if ( !item.hasClass( "ui-state-disabled" ) ) { + this._select( item.data( "ui-selectmenu-item" ), event ); + } + }, - // Fix precision from bad JS floating point math - value = parseFloat( value.toFixed( this._precision() ) ); + _select: function( item, event ) { + var oldIndex = this.element[ 0 ].selectedIndex; - // Clamp the value - if ( options.max !== null && value > options.max ) { - return options.max; - } - if ( options.min !== null && value < options.min ) { - return options.min; + // Change native select element + this.element[ 0 ].selectedIndex = item.index; + this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) ); + this._setAria( item ); + this._trigger( "select", event, { item: item } ); + + if ( item.index !== oldIndex ) { + this._trigger( "change", event, { item: item } ); } - return value; + this.close( event ); }, - _stop: function( event ) { - if ( !this.spinning ) { - return; - } + _setAria: function( item ) { + var id = this.menuItems.eq( item.index ).attr( "id" ); - clearTimeout( this.timer ); - clearTimeout( this.mousewheelTimer ); - this.counter = 0; - this.spinning = false; - this._trigger( "stop", event ); + this.button.attr( { + "aria-labelledby": id, + "aria-activedescendant": id + } ); + this.menu.attr( "aria-activedescendant", id ); }, _setOption: function( key, value ) { - var prevValue, first, last; - - if ( key === "culture" || key === "numberFormat" ) { - prevValue = this._parse( this.element.val() ); - this.options[ key ] = value; - this.element.val( this._format( prevValue ) ); - return; - } - - if ( key === "max" || key === "min" || key === "step" ) { - if ( typeof value === "string" ) { - value = this._parse( value ); - } - } if ( key === "icons" ) { - first = this.buttons.first().find( ".ui-icon" ); - this._removeClass( first, null, this.options.icons.up ); - this._addClass( first, null, value.up ); - last = this.buttons.last().find( ".ui-icon" ); - this._removeClass( last, null, this.options.icons.down ); - this._addClass( last, null, value.down ); + var icon = this.button.find( "span.ui-icon" ); + this._removeClass( icon, null, this.options.icons.button ) + ._addClass( icon, null, value.button ); } this._super( key, value ); + + if ( key === "appendTo" ) { + this.menuWrap.appendTo( this._appendTo() ); + } + + if ( key === "width" ) { + this._resizeButton(); + } }, _setOptionDisabled: function( value ) { this._super( value ); - this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value ); - this.element.prop( "disabled", !!value ); - this.buttons.button( value ? "disable" : "enable" ); + this.menuInstance.option( "disabled", value ); + this.button.attr( "aria-disabled", value ); + this._toggleClass( this.button, null, "ui-state-disabled", value ); + + this.element.prop( "disabled", value ); + if ( value ) { + this.button.attr( "tabindex", -1 ); + this.close(); + } else { + this.button.attr( "tabindex", 0 ); + } }, - _setOptions: spinnerModifier( function( options ) { - this._super( options ); - } ), + _appendTo: function() { + var element = this.options.appendTo; - _parse: function( val ) { - if ( typeof val === "string" && val !== "" ) { - val = window.Globalize && this.options.numberFormat ? - Globalize.parseFloat( val, 10, this.options.culture ) : +val; + if ( element ) { + element = element.jquery || element.nodeType ? + $( element ) : + this.document.find( element ).eq( 0 ); } - return val === "" || isNaN( val ) ? null : val; - }, - _format: function( value ) { - if ( value === "" ) { - return ""; + if ( !element || !element[ 0 ] ) { + element = this.element.closest( ".ui-front, dialog" ); } - return window.Globalize && this.options.numberFormat ? - Globalize.format( value, this.options.numberFormat, this.options.culture ) : - value; - }, - _refresh: function() { - this.element.attr( { - "aria-valuemin": this.options.min, - "aria-valuemax": this.options.max, + if ( !element.length ) { + element = this.document[ 0 ].body; + } - // TODO: what should we do with values that can't be parsed? - "aria-valuenow": this._parse( this.element.val() ) - } ); + return element; }, - isValid: function() { - var value = this.value(); + _toggleAttr: function() { + this.button.attr( "aria-expanded", this.isOpen ); - // Null is invalid - if ( value === null ) { - return false; - } + // We can't use two _toggleClass() calls here, because we need to make sure + // we always remove classes first and add them second, otherwise if both classes have the + // same theme class, it will be removed after we add it. + this._removeClass( this.button, "ui-selectmenu-button-" + + ( this.isOpen ? "closed" : "open" ) ) + ._addClass( this.button, "ui-selectmenu-button-" + + ( this.isOpen ? "open" : "closed" ) ) + ._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen ); - // If value gets adjusted, it's invalid - return value === this._adjustValue( value ); + this.menu.attr( "aria-hidden", !this.isOpen ); }, - // Update the value without triggering change - _value: function( value, allowAny ) { - var parsed; - if ( value !== "" ) { - parsed = this._parse( value ); - if ( parsed !== null ) { - if ( !allowAny ) { - parsed = this._adjustValue( parsed ); - } - value = this._format( parsed ); - } + _resizeButton: function() { + var width = this.options.width; + + // For `width: false`, just remove inline style and stop + if ( width === false ) { + this.button.css( "width", "" ); + return; } - this.element.val( value ); - this._refresh(); - }, - _destroy: function() { - this.element - .prop( "disabled", false ) - .removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" ); + // For `width: null`, match the width of the original element + if ( width === null ) { + width = this.element.show().outerWidth(); + this.element.hide(); + } - this.uiSpinner.replaceWith( this.element ); + this.button.outerWidth( width ); }, - stepUp: spinnerModifier( function( steps ) { - this._stepUp( steps ); - } ), - _stepUp: function( steps ) { - if ( this._start() ) { - this._spin( ( steps || 1 ) * this.options.step ); - this._stop(); - } - }, + _resizeMenu: function() { + this.menu.outerWidth( Math.max( + this.button.outerWidth(), - stepDown: spinnerModifier( function( steps ) { - this._stepDown( steps ); - } ), - _stepDown: function( steps ) { - if ( this._start() ) { - this._spin( ( steps || 1 ) * -this.options.step ); - this._stop(); - } + // Support: IE10 + // IE10 wraps long text (possibly a rounding bug) + // so we add 1px to avoid the wrapping + this.menu.width( "" ).outerWidth() + 1 + ) ); }, - pageUp: spinnerModifier( function( pages ) { - this._stepUp( ( pages || 1 ) * this.options.page ); - } ), + _getCreateOptions: function() { + var options = this._super(); - pageDown: spinnerModifier( function( pages ) { - this._stepDown( ( pages || 1 ) * this.options.page ); - } ), + options.disabled = this.element.prop( "disabled" ); - value: function( newVal ) { - if ( !arguments.length ) { - return this._parse( this.element.val() ); - } - spinnerModifier( this._value ).call( this, newVal ); + return options; }, - widget: function() { - return this.uiSpinner; - } -} ); - -// DEPRECATED -// TODO: switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { - - // Backcompat for spinner html extension points - $.widget( "ui.spinner", $.ui.spinner, { - _enhance: function() { - this.uiSpinner = this.element - .attr( "autocomplete", "off" ) - .wrap( this._uiSpinnerHtml() ) - .parent() + _parseOptions: function( options ) { + var that = this, + data = []; + options.each( function( index, item ) { + if ( item.hidden ) { + return; + } - // Add buttons - .append( this._buttonHtml() ); - }, - _uiSpinnerHtml: function() { - return "<span>"; - }, + data.push( that._parseOption( $( item ), index ) ); + } ); + this.items = data; + }, - _buttonHtml: function() { - return "<a></a><a></a>"; - } - } ); -} + _parseOption: function( option, index ) { + var optgroup = option.parent( "optgroup" ); -var widgetsSpinner = $.ui.spinner; + return { + element: option, + index: index, + value: option.val(), + label: option.text(), + optgroup: optgroup.attr( "label" ) || "", + disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" ) + }; + }, + + _destroy: function() { + this._unbindFormResetHandler(); + this.menuWrap.remove(); + this.button.remove(); + this.element.show(); + this.element.removeUniqueId(); + this.labels.attr( "for", this.ids.element ); + } +} ] ); /*! - * jQuery UI Tabs 1.13.1 + * jQuery UI Slider 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -15043,3579 +14767,3792 @@ var widgetsSpinner = $.ui.spinner; * http://jquery.org/license */ -//>>label: Tabs +//>>label: Slider //>>group: Widgets -//>>description: Transforms a set of container elements into a tab structure. -//>>docs: http://api.jqueryui.com/tabs/ -//>>demos: http://jqueryui.com/tabs/ +//>>description: Displays a flexible slider with ranges and accessibility via keyboard. +//>>docs: http://api.jqueryui.com/slider/ +//>>demos: http://jqueryui.com/slider/ //>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/tabs.css +//>>css.structure: ../../themes/base/slider.css //>>css.theme: ../../themes/base/theme.css -$.widget( "ui.tabs", { - version: "1.13.1", - delay: 300, +var widgetsSlider = $.widget( "ui.slider", $.ui.mouse, { + version: "1.13.2", + widgetEventPrefix: "slide", + options: { - active: null, + animate: false, classes: { - "ui-tabs": "ui-corner-all", - "ui-tabs-nav": "ui-corner-all", - "ui-tabs-panel": "ui-corner-bottom", - "ui-tabs-tab": "ui-corner-top" + "ui-slider": "ui-corner-all", + "ui-slider-handle": "ui-corner-all", + + // Note: ui-widget-header isn't the most fittingly semantic framework class for this + // element, but worked best visually with a variety of themes + "ui-slider-range": "ui-corner-all ui-widget-header" }, - collapsible: false, - event: "click", - heightStyle: "content", - hide: null, - show: null, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null, // Callbacks - activate: null, - beforeActivate: null, - beforeLoad: null, - load: null + change: null, + slide: null, + start: null, + stop: null }, - _isLocal: ( function() { - var rhash = /#.*$/; - - return function( anchor ) { - var anchorUrl, locationUrl; + // Number of pages in a slider + // (how many times can you page up/down to go through the whole range) + numPages: 5, - anchorUrl = anchor.href.replace( rhash, "" ); - locationUrl = location.href.replace( rhash, "" ); + _create: function() { + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + this._calculateNewMax(); - // Decoding may throw an error if the URL isn't UTF-8 (#9518) - try { - anchorUrl = decodeURIComponent( anchorUrl ); - } catch ( error ) {} - try { - locationUrl = decodeURIComponent( locationUrl ); - } catch ( error ) {} + this._addClass( "ui-slider ui-slider-" + this.orientation, + "ui-widget ui-widget-content" ); - return anchor.hash.length > 1 && anchorUrl === locationUrl; - }; - } )(), + this._refresh(); - _create: function() { - var that = this, - options = this.options; + this._animateOff = false; + }, - this.running = false; + _refresh: function() { + this._createRange(); + this._createHandles(); + this._setupEvents(); + this._refreshValue(); + }, - this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); - this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); + _createHandles: function() { + var i, handleCount, + options = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ), + handle = "<span tabindex='0'></span>", + handles = []; - this._processTabs(); - options.active = this._initialActive(); + handleCount = ( options.values && options.values.length ) || 1; - // Take disabling tabs via class attribute from HTML - // into account and update option properly. - if ( Array.isArray( options.disabled ) ) { - options.disabled = $.uniqueSort( options.disabled.concat( - $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { - return that.tabs.index( li ); - } ) - ) ).sort(); + if ( existingHandles.length > handleCount ) { + existingHandles.slice( handleCount ).remove(); + existingHandles = existingHandles.slice( 0, handleCount ); } - // Check for length avoids error when initializing empty list - if ( this.options.active !== false && this.anchors.length ) { - this.active = this._findActive( options.active ); - } else { - this.active = $(); + for ( i = existingHandles.length; i < handleCount; i++ ) { + handles.push( handle ); } - this._refresh(); + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); - if ( this.active.length ) { - this.load( options.active ); - } + this._addClass( this.handles, "ui-slider-handle", "ui-state-default" ); + + this.handle = this.handles.eq( 0 ); + + this.handles.each( function( i ) { + $( this ) + .data( "ui-slider-handle-index", i ) + .attr( "tabIndex", 0 ); + } ); }, - _initialActive: function() { - var active = this.options.active, - collapsible = this.options.collapsible, - locationHash = location.hash.substring( 1 ); + _createRange: function() { + var options = this.options; - if ( active === null ) { + if ( options.range ) { + if ( options.range === true ) { + if ( !options.values ) { + options.values = [ this._valueMin(), this._valueMin() ]; + } else if ( options.values.length && options.values.length !== 2 ) { + options.values = [ options.values[ 0 ], options.values[ 0 ] ]; + } else if ( Array.isArray( options.values ) ) { + options.values = options.values.slice( 0 ); + } + } - // check the fragment identifier in the URL - if ( locationHash ) { - this.tabs.each( function( i, tab ) { - if ( $( tab ).attr( "aria-controls" ) === locationHash ) { - active = i; - return false; - } + if ( !this.range || !this.range.length ) { + this.range = $( "<div>" ) + .appendTo( this.element ); + + this._addClass( this.range, "ui-slider-range" ); + } else { + this._removeClass( this.range, "ui-slider-range-min ui-slider-range-max" ); + + // Handle range switching from true to min/max + this.range.css( { + "left": "", + "bottom": "" } ); } - - // Check for a tab marked active via a class - if ( active === null ) { - active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); + if ( options.range === "min" || options.range === "max" ) { + this._addClass( this.range, "ui-slider-range-" + options.range ); + } + } else { + if ( this.range ) { + this.range.remove(); } + this.range = null; + } + }, - // No active tab, set to false - if ( active === null || active === -1 ) { - active = this.tabs.length ? 0 : false; + _setupEvents: function() { + this._off( this.handles ); + this._on( this.handles, this._handleEvents ); + this._hoverable( this.handles ); + this._focusable( this.handles ); + }, + + _destroy: function() { + this.handles.remove(); + if ( this.range ) { + this.range.remove(); + } + + this._mouseDestroy(); + }, + + _mouseCapture: function( event ) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each( function( i ) { + var thisDistance = Math.abs( normValue - that.values( i ) ); + if ( ( distance > thisDistance ) || + ( distance === thisDistance && + ( i === that._lastChangedValue || that.values( i ) === o.min ) ) ) { + distance = thisDistance; + closestHandle = $( this ); + index = i; } - } + } ); - // Handle numbers: negative, out of range - if ( active !== false ) { - active = this.tabs.index( this.tabs.eq( active ) ); - if ( active === -1 ) { - active = collapsible ? false : 0; - } + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; } + this._mouseSliding = true; - // Don't allow collapsible: false and active: false - if ( !collapsible && active === false && this.anchors.length ) { - active = 0; - } + this._handleIndex = index; - return active; - }, + this._addClass( closestHandle, null, "ui-state-active" ); + closestHandle.trigger( "focus" ); - _getCreateEventData: function() { - return { - tab: this.active, - panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css( "borderTopWidth" ), 10 ) || 0 ) - + ( parseInt( closestHandle.css( "borderBottomWidth" ), 10 ) || 0 ) + + ( parseInt( closestHandle.css( "marginTop" ), 10 ) || 0 ) }; - }, - - _tabKeydown: function( event ) { - var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), - selectedIndex = this.tabs.index( focusedTab ), - goingForward = true; - if ( this._handlePageNav( event ) ) { - return; + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); } + this._animateOff = true; + return true; + }, - switch ( event.keyCode ) { - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - selectedIndex++; - break; - case $.ui.keyCode.UP: - case $.ui.keyCode.LEFT: - goingForward = false; - selectedIndex--; - break; - case $.ui.keyCode.END: - selectedIndex = this.anchors.length - 1; - break; - case $.ui.keyCode.HOME: - selectedIndex = 0; - break; - case $.ui.keyCode.SPACE: + _mouseStart: function() { + return true; + }, - // Activate only, no collapsing - event.preventDefault(); - clearTimeout( this.activating ); - this._activate( selectedIndex ); - return; - case $.ui.keyCode.ENTER: + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); - // Toggle (cancel delayed activation, allow collapsing) - event.preventDefault(); - clearTimeout( this.activating ); + this._slide( event, this._handleIndex, normValue ); - // Determine if we should collapse or activate - this._activate( selectedIndex === this.options.active ? false : selectedIndex ); - return; - default: - return; - } + return false; + }, - // Focus the appropriate tab, based on which key was pressed - event.preventDefault(); - clearTimeout( this.activating ); - selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + _mouseStop: function( event ) { + this._removeClass( this.handles, null, "ui-state-active" ); + this._mouseSliding = false; - // Navigating with control/command key will prevent automatic activation - if ( !event.ctrlKey && !event.metaKey ) { + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); - // Update aria-selected immediately so that AT think the tab is already selected. - // Otherwise AT may confuse the user by stating that they need to activate the tab, - // but the tab will already be activated by the time the announcement finishes. - focusedTab.attr( "aria-selected", "false" ); - this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; - this.activating = this._delay( function() { - this.option( "active", selectedIndex ); - }, this.delay ); - } + return false; }, - _panelKeydown: function( event ) { - if ( this._handlePageNav( event ) ) { - return; - } + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, - // Ctrl+up moves focus to the current tab - if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { - event.preventDefault(); - this.active.trigger( "focus" ); + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - + ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - + ( this._clickOffset ? this._clickOffset.top : 0 ); } - }, - // Alt+page up/down moves focus to the previous/next tab (and activates) - _handlePageNav: function( event ) { - if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { - this._activate( this._focusNextTab( this.options.active - 1, false ) ); - return true; + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; } - if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { - this._activate( this._focusNextTab( this.options.active + 1, true ) ); - return true; + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; } - }, - _findNextTab: function( index, goingForward ) { - var lastTabIndex = this.tabs.length - 1; + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; - function constrain() { - if ( index > lastTabIndex ) { - index = 0; - } - if ( index < 0 ) { - index = lastTabIndex; - } - return index; - } + return this._trimAlignValue( valueMouse ); + }, - while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { - index = goingForward ? index + 1 : index - 1; + _uiHash: function( index, value, values ) { + var uiHash = { + handle: this.handles[ index ], + handleIndex: index, + value: value !== undefined ? value : this.value() + }; + + if ( this._hasMultipleValues() ) { + uiHash.value = value !== undefined ? value : this.values( index ); + uiHash.values = values || this.values(); } - return index; + return uiHash; }, - _focusNextTab: function( index, goingForward ) { - index = this._findNextTab( index, goingForward ); - this.tabs.eq( index ).trigger( "focus" ); - return index; + _hasMultipleValues: function() { + return this.options.values && this.options.values.length; }, - _setOption: function( key, value ) { - if ( key === "active" ) { - - // _activate() will handle invalid values and update this.options - this._activate( value ); - return; - } + _start: function( event, index ) { + return this._trigger( "start", event, this._uiHash( index ) ); + }, - this._super( key, value ); + _slide: function( event, index, newVal ) { + var allowed, otherVal, + currentValue = this.value(), + newValues = this.values(); - if ( key === "collapsible" ) { - this._toggleClass( "ui-tabs-collapsible", null, value ); + if ( this._hasMultipleValues() ) { + otherVal = this.values( index ? 0 : 1 ); + currentValue = this.values( index ); - // Setting collapsible: false while collapsed; open first panel - if ( !value && this.options.active === false ) { - this._activate( 0 ); + if ( this.options.values.length === 2 && this.options.range === true ) { + newVal = index === 0 ? Math.min( otherVal, newVal ) : Math.max( otherVal, newVal ); } - } - if ( key === "event" ) { - this._setupEvents( value ); + newValues[ index ] = newVal; } - if ( key === "heightStyle" ) { - this._setupHeightStyle( value ); + if ( newVal === currentValue ) { + return; } - }, - - _sanitizeSelector: function( hash ) { - return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; - }, - - refresh: function() { - var options = this.options, - lis = this.tablist.children( ":has(a[href])" ); - - // Get disabled tabs from class attribute from HTML - // this will get converted to a boolean if needed in _refresh() - options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { - return lis.index( tab ); - } ); - this._processTabs(); + allowed = this._trigger( "slide", event, this._uiHash( index, newVal, newValues ) ); - // Was collapsed or no tabs - if ( options.active === false || !this.anchors.length ) { - options.active = false; - this.active = $(); + // A slide can be canceled by returning false from the slide callback + if ( allowed === false ) { + return; + } - // was active, but active tab is gone - } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { + if ( this._hasMultipleValues() ) { + this.values( index, newVal ); + } else { + this.value( newVal ); + } + }, - // all remaining tabs are disabled - if ( this.tabs.length === options.disabled.length ) { - options.active = false; - this.active = $(); + _stop: function( event, index ) { + this._trigger( "stop", event, this._uiHash( index ) ); + }, - // activate previous tab - } else { - this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); - } + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { - // was active, active tab still exists - } else { + //store the last changed value index for reference when handles overlap + this._lastChangedValue = index; + this._trigger( "change", event, this._uiHash( index ) ); + } + }, - // make sure active index is correct - options.active = this.tabs.index( this.active ); + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; } - this._refresh(); + return this._value(); }, - _refresh: function() { - this._setOptionDisabled( this.options.disabled ); - this._setupEvents( this.options.event ); - this._setupHeightStyle( this.options.heightStyle ); + values: function( index, newValue ) { + var vals, + newValues, + i; - this.tabs.not( this.active ).attr( { - "aria-selected": "false", - "aria-expanded": "false", - tabIndex: -1 - } ); - this.panels.not( this._getPanelForTab( this.active ) ) - .hide() - .attr( { - "aria-hidden": "true" - } ); + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } - // Make sure one tab is in the tab order - if ( !this.active.length ) { - this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + if ( arguments.length ) { + if ( Array.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this._hasMultipleValues() ) { + return this._values( index ); + } else { + return this.value(); + } + } } else { - this.active - .attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); - this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); - this._getPanelForTab( this.active ) - .show() - .attr( { - "aria-hidden": "false" - } ); + return this._values(); } }, - _processTabs: function() { - var that = this, - prevTabs = this.tabs, - prevAnchors = this.anchors, - prevPanels = this.panels; + _setOption: function( key, value ) { + var i, + valsLength = 0; - this.tablist = this._getList().attr( "role", "tablist" ); - this._addClass( this.tablist, "ui-tabs-nav", - "ui-helper-reset ui-helper-clearfix ui-widget-header" ); + if ( key === "range" && this.options.range === true ) { + if ( value === "min" ) { + this.options.value = this._values( 0 ); + this.options.values = null; + } else if ( value === "max" ) { + this.options.value = this._values( this.options.values.length - 1 ); + this.options.values = null; + } + } - // Prevent users from focusing disabled tabs via click - this.tablist - .on( "mousedown" + this.eventNamespace, "> li", function( event ) { - if ( $( this ).is( ".ui-state-disabled" ) ) { - event.preventDefault(); + if ( Array.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + this._super( key, value ); + + switch ( key ) { + case "orientation": + this._detectOrientation(); + this._removeClass( "ui-slider-horizontal ui-slider-vertical" ) + ._addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + if ( this.options.range ) { + this._refreshRange( value ); } - } ) - // Support: IE <9 - // Preventing the default action in mousedown doesn't prevent IE - // from focusing the element, so if the anchor gets focused, blur. - // We don't have to worry about focusing the previously focused - // element since clicking on a non-focusable element should focus - // the body anyway. - .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { - if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { - this.blur(); + // Reset positioning from previous orientation + this.handles.css( value === "horizontal" ? "bottom" : "left", "" ); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + + // Start from the last handle to prevent unreachable handles (#9046) + for ( i = valsLength - 1; i >= 0; i-- ) { + this._change( null, i ); } - } ); + this._animateOff = false; + break; + case "step": + case "min": + case "max": + this._animateOff = true; + this._calculateNewMax(); + this._refreshValue(); + this._animateOff = false; + break; + case "range": + this._animateOff = true; + this._refresh(); + this._animateOff = false; + break; + } + }, - this.tabs = this.tablist.find( "> li:has(a[href])" ) - .attr( { - role: "tab", - tabIndex: -1 - } ); - this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); + _setOptionDisabled: function( value ) { + this._super( value ); - this.anchors = this.tabs.map( function() { - return $( "a", this )[ 0 ]; - } ) - .attr( { - tabIndex: -1 - } ); - this._addClass( this.anchors, "ui-tabs-anchor" ); + this._toggleClass( null, "ui-state-disabled", !!value ); + }, - this.panels = $(); + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); - this.anchors.each( function( i, anchor ) { - var selector, panel, panelId, - anchorId = $( anchor ).uniqueId().attr( "id" ), - tab = $( anchor ).closest( "li" ), - originalAriaControls = tab.attr( "aria-controls" ); + return val; + }, - // Inline tab - if ( that._isLocal( anchor ) ) { - selector = anchor.hash; - panelId = selector.substring( 1 ); - panel = that.element.find( that._sanitizeSelector( selector ) ); + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; - // remote tab - } else { + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); - // If the tab doesn't already have aria-controls, - // generate an id by using a throw-away element - panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; - selector = "#" + panelId; - panel = that.element.find( selector ); - if ( !panel.length ) { - panel = that._createPanel( panelId ); - panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); - } - panel.attr( "aria-live", "polite" ); - } + return val; + } else if ( this._hasMultipleValues() ) { - if ( panel.length ) { - that.panels = that.panels.add( panel ); - } - if ( originalAriaControls ) { - tab.data( "ui-tabs-aria-controls", originalAriaControls ); + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); } - tab.attr( { - "aria-controls": panelId, - "aria-labelledby": anchorId - } ); - panel.attr( "aria-labelledby", anchorId ); - } ); - - this.panels.attr( "role", "tabpanel" ); - this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); - // Avoid memory leaks (#10056) - if ( prevTabs ) { - this._off( prevTabs.not( this.tabs ) ); - this._off( prevAnchors.not( this.anchors ) ); - this._off( prevPanels.not( this.panels ) ); + return vals; + } else { + return []; } }, - // Allow overriding how to find the list for rare usage scenarios (#7715) - _getList: function() { - return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); - }, + // Returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = ( val - this._valueMin() ) % step, + alignValue = val - valModStep; - _createPanel: function( id ) { - return $( "<div>" ) - .attr( "id", id ) - .data( "ui-tabs-destroy", true ); + if ( Math.abs( valModStep ) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed( 5 ) ); }, - _setOptionDisabled: function( disabled ) { - var currentItem, li, i; + _calculateNewMax: function() { + var max = this.options.max, + min = this._valueMin(), + step = this.options.step, + aboveMin = Math.round( ( max - min ) / step ) * step; + max = aboveMin + min; + if ( max > this.options.max ) { - if ( Array.isArray( disabled ) ) { - if ( !disabled.length ) { - disabled = false; - } else if ( disabled.length === this.anchors.length ) { - disabled = true; - } + //If max is not divisible by step, rounding off may increase its value + max -= step; } + this.max = parseFloat( max.toFixed( this._precision() ) ); + }, - // Disable tabs - for ( i = 0; ( li = this.tabs[ i ] ); i++ ) { - currentItem = $( li ); - if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { - currentItem.attr( "aria-disabled", "true" ); - this._addClass( currentItem, null, "ui-state-disabled" ); - } else { - currentItem.removeAttr( "aria-disabled" ); - this._removeClass( currentItem, null, "ui-state-disabled" ); - } + _precision: function() { + var precision = this._precisionOf( this.options.step ); + if ( this.options.min !== null ) { + precision = Math.max( precision, this._precisionOf( this.options.min ) ); } + return precision; + }, - this.options.disabled = disabled; + _precisionOf: function( num ) { + var str = num.toString(), + decimal = str.indexOf( "." ); + return decimal === -1 ? 0 : str.length - decimal - 1; + }, - this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, - disabled === true ); + _valueMin: function() { + return this.options.min; }, - _setupEvents: function( event ) { - var events = {}; - if ( event ) { - $.each( event.split( " " ), function( index, eventName ) { - events[ eventName ] = "_eventHandler"; - } ); + _valueMax: function() { + return this.max; + }, + + _refreshRange: function( orientation ) { + if ( orientation === "vertical" ) { + this.range.css( { "width": "", "left": "" } ); + } + if ( orientation === "horizontal" ) { + this.range.css( { "height": "", "bottom": "" } ); } + }, - this._off( this.anchors.add( this.tabs ).add( this.panels ) ); + _refreshValue: function() { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = ( !this._animateOff ) ? o.animate : false, + _set = {}; - // Always prevent the default action, even when disabled - this._on( true, this.anchors, { - click: function( event ) { - event.preventDefault(); - } - } ); - this._on( this.anchors, events ); - this._on( this.tabs, { keydown: "_tabKeydown" } ); - this._on( this.panels, { keydown: "_panelKeydown" } ); + if ( this._hasMultipleValues() ) { + this.handles.each( function( i ) { + valPercent = ( that.values( i ) - that._valueMin() ) / ( that._valueMax() - + that._valueMin() ) * 100; + _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( that.options.range === true ) { + if ( that.orientation === "horizontal" ) { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + left: valPercent + "%" + }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { + width: ( valPercent - lastValPercent ) + "%" + }, { + queue: false, + duration: o.animate + } ); + } + } else { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + bottom: ( valPercent ) + "%" + }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { + height: ( valPercent - lastValPercent ) + "%" + }, { + queue: false, + duration: o.animate + } ); + } + } + } + lastValPercent = valPercent; + } ); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - this._focusable( this.tabs ); - this._hoverable( this.tabs ); + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + width: valPercent + "%" + }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + width: ( 100 - valPercent ) + "%" + }, o.animate ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + height: valPercent + "%" + }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { + height: ( 100 - valPercent ) + "%" + }, o.animate ); + } + } }, - _setupHeightStyle: function( heightStyle ) { - var maxHeight, - parent = this.element.parent(); + _handleEvents: { + keydown: function( event ) { + var allowed, curVal, newVal, step, + index = $( event.target ).data( "ui-slider-handle-index" ); - if ( heightStyle === "fill" ) { - maxHeight = parent.height(); - maxHeight -= this.element.outerHeight() - this.element.height(); + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !this._keySliding ) { + this._keySliding = true; + this._addClass( $( event.target ), null, "ui-state-active" ); + allowed = this._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } - this.element.siblings( ":visible" ).each( function() { - var elem = $( this ), - position = elem.css( "position" ); + step = this.options.step; + if ( this._hasMultipleValues() ) { + curVal = newVal = this.values( index ); + } else { + curVal = newVal = this.value(); + } - if ( position === "absolute" || position === "fixed" ) { - return; - } - maxHeight -= elem.outerHeight( true ); - } ); + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue( + curVal + ( ( this._valueMax() - this._valueMin() ) / this.numPages ) + ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue( + curVal - ( ( this._valueMax() - this._valueMin() ) / this.numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === this._valueMax() ) { + return; + } + newVal = this._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === this._valueMin() ) { + return; + } + newVal = this._trimAlignValue( curVal - step ); + break; + } - this.element.children().not( this.panels ).each( function() { - maxHeight -= $( this ).outerHeight( true ); - } ); + this._slide( event, index, newVal ); + }, + keyup: function( event ) { + var index = $( event.target ).data( "ui-slider-handle-index" ); - this.panels.each( function() { - $( this ).height( Math.max( 0, maxHeight - - $( this ).innerHeight() + $( this ).height() ) ); - } ) - .css( "overflow", "auto" ); - } else if ( heightStyle === "auto" ) { - maxHeight = 0; - this.panels.each( function() { - maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); - } ).height( maxHeight ); + if ( this._keySliding ) { + this._keySliding = false; + this._stop( event, index ); + this._change( event, index ); + this._removeClass( $( event.target ), null, "ui-state-active" ); + } } - }, + } +} ); - _eventHandler: function( event ) { - var options = this.options, - active = this.active, - anchor = $( event.currentTarget ), - tab = anchor.closest( "li" ), - clickedIsActive = tab[ 0 ] === active[ 0 ], - collapsing = clickedIsActive && options.collapsible, - toShow = collapsing ? $() : this._getPanelForTab( tab ), - toHide = !active.length ? $() : this._getPanelForTab( active ), - eventData = { - oldTab: active, - oldPanel: toHide, - newTab: collapsing ? $() : tab, - newPanel: toShow - }; - event.preventDefault(); +/*! + * jQuery UI Sortable 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( tab.hasClass( "ui-state-disabled" ) || +//>>label: Sortable +//>>group: Interactions +//>>description: Enables items in a list to be sorted using the mouse. +//>>docs: http://api.jqueryui.com/sortable/ +//>>demos: http://jqueryui.com/sortable/ +//>>css.structure: ../../themes/base/sortable.css - // tab is already loading - tab.hasClass( "ui-tabs-loading" ) || - // can't switch durning an animation - this.running || +var widgetsSortable = $.widget( "ui.sortable", $.ui.mouse, { + version: "1.13.2", + widgetEventPrefix: "sort", + ready: false, + options: { + appendTo: "parent", + axis: false, + connectWith: false, + containment: false, + cursor: "auto", + cursorAt: false, + dropOnEmpty: true, + forcePlaceholderSize: false, + forceHelperSize: false, + grid: false, + handle: false, + helper: "original", + items: "> *", + opacity: false, + placeholder: false, + revert: false, + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + scope: "default", + tolerance: "intersect", + zIndex: 1000, - // click on active header, but not collapsible - ( clickedIsActive && !options.collapsible ) || + // Callbacks + activate: null, + beforeStop: null, + change: null, + deactivate: null, + out: null, + over: null, + receive: null, + remove: null, + sort: null, + start: null, + stop: null, + update: null + }, - // allow canceling activation - ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { - return; - } + _isOverAxis: function( x, reference, size ) { + return ( x >= reference ) && ( x < ( reference + size ) ); + }, - options.active = collapsing ? false : this.tabs.index( tab ); + _isFloating: function( item ) { + return ( /left|right/ ).test( item.css( "float" ) ) || + ( /inline|table-cell/ ).test( item.css( "display" ) ); + }, - this.active = clickedIsActive ? $() : tab; - if ( this.xhr ) { - this.xhr.abort(); - } + _create: function() { + this.containerCache = {}; + this._addClass( "ui-sortable" ); - if ( !toHide.length && !toShow.length ) { - $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); - } + //Get the items + this.refresh(); - if ( toShow.length ) { - this.load( this.tabs.index( tab ), event ); - } - this._toggle( event, eventData ); - }, + //Let's determine the parent's offset + this.offset = this.element.offset(); - // Handles show/hide for selecting tabs - _toggle: function( event, eventData ) { - var that = this, - toShow = eventData.newPanel, - toHide = eventData.oldPanel; + //Initialize mouse events for interaction + this._mouseInit(); - this.running = true; + this._setHandleClassName(); - function complete() { - that.running = false; - that._trigger( "activate", event, eventData ); - } + //We're ready to go + this.ready = true; - function show() { - that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" ); + }, - if ( toShow.length && that.options.show ) { - that._show( toShow, that.options.show, complete ); - } else { - toShow.show(); - complete(); - } - } + _setOption: function( key, value ) { + this._super( key, value ); - // Start out by hiding, then showing, then completing - if ( toHide.length && this.options.hide ) { - this._hide( toHide, this.options.hide, function() { - that._removeClass( eventData.oldTab.closest( "li" ), - "ui-tabs-active", "ui-state-active" ); - show(); - } ); - } else { - this._removeClass( eventData.oldTab.closest( "li" ), - "ui-tabs-active", "ui-state-active" ); - toHide.hide(); - show(); + if ( key === "handle" ) { + this._setHandleClassName(); } + }, - toHide.attr( "aria-hidden", "true" ); - eventData.oldTab.attr( { - "aria-selected": "false", - "aria-expanded": "false" + _setHandleClassName: function() { + var that = this; + this._removeClass( this.element.find( ".ui-sortable-handle" ), "ui-sortable-handle" ); + $.each( this.items, function() { + that._addClass( + this.instance.options.handle ? + this.item.find( this.instance.options.handle ) : + this.item, + "ui-sortable-handle" + ); } ); + }, - // If we're switching tabs, remove the old tab from the tab order. - // If we're opening from collapsed state, remove the previous tab from the tab order. - // If we're collapsing, then keep the collapsing tab in the tab order. - if ( toShow.length && toHide.length ) { - eventData.oldTab.attr( "tabIndex", -1 ); - } else if ( toShow.length ) { - this.tabs.filter( function() { - return $( this ).attr( "tabIndex" ) === 0; - } ) - .attr( "tabIndex", -1 ); + _destroy: function() { + this._mouseDestroy(); + + for ( var i = this.items.length - 1; i >= 0; i-- ) { + this.items[ i ].item.removeData( this.widgetName + "-item" ); } - toShow.attr( "aria-hidden", "false" ); - eventData.newTab.attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); + return this; }, - _activate: function( index ) { - var anchor, - active = this._findActive( index ); + _mouseCapture: function( event, overrideHandle ) { + var currentItem = null, + validHandle = false, + that = this; - // Trying to activate the already active panel - if ( active[ 0 ] === this.active[ 0 ] ) { - return; + if ( this.reverting ) { + return false; } - // Trying to collapse, simulate a click on the current active header - if ( !active.length ) { - active = this.active; + if ( this.options.disabled || this.options.type === "static" ) { + return false; } - anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; - this._eventHandler( { - target: anchor, - currentTarget: anchor, - preventDefault: $.noop + //We have to refresh the items data once first + this._refreshItems( event ); + + //Find out if the clicked node (or one of its parents) is a actual item in this.items + $( event.target ).parents().each( function() { + if ( $.data( this, that.widgetName + "-item" ) === that ) { + currentItem = $( this ); + return false; + } } ); - }, + if ( $.data( event.target, that.widgetName + "-item" ) === that ) { + currentItem = $( event.target ); + } + + if ( !currentItem ) { + return false; + } + if ( this.options.handle && !overrideHandle ) { + $( this.options.handle, currentItem ).find( "*" ).addBack().each( function() { + if ( this === event.target ) { + validHandle = true; + } + } ); + if ( !validHandle ) { + return false; + } + } + + this.currentItem = currentItem; + this._removeCurrentsFromItems(); + return true; - _findActive: function( index ) { - return index === false ? $() : this.tabs.eq( index ); }, - _getIndex: function( index ) { + _mouseStart: function( event, overrideHandle, noActivation ) { - // meta-function to give users option to provide a href string instead of a numerical index. - if ( typeof index === "string" ) { - index = this.anchors.index( this.anchors.filter( "[href$='" + - $.escapeSelector( index ) + "']" ) ); - } + var i, body, + o = this.options; - return index; - }, + this.currentContainer = this; - _destroy: function() { - if ( this.xhr ) { - this.xhr.abort(); - } + //We only need to call refreshPositions, because the refreshItems call has been moved to + // mouseCapture + this.refreshPositions(); - this.tablist - .removeAttr( "role" ) - .off( this.eventNamespace ); + //Prepare the dragged items parent + this.appendTo = $( o.appendTo !== "parent" ? + o.appendTo : + this.currentItem.parent() ); - this.anchors - .removeAttr( "role tabIndex" ) - .removeUniqueId(); + //Create and append the visible helper + this.helper = this._createHelper( event ); - this.tabs.add( this.panels ).each( function() { - if ( $.data( this, "ui-tabs-destroy" ) ) { - $( this ).remove(); - } else { - $( this ).removeAttr( "role tabIndex " + - "aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" ); - } - } ); + //Cache the helper size + this._cacheHelperProportions(); - this.tabs.each( function() { - var li = $( this ), - prev = li.data( "ui-tabs-aria-controls" ); - if ( prev ) { - li - .attr( "aria-controls", prev ) - .removeData( "ui-tabs-aria-controls" ); - } else { - li.removeAttr( "aria-controls" ); - } - } ); + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ - this.panels.show(); + //Cache the margins of the original element + this._cacheMargins(); - if ( this.options.heightStyle !== "content" ) { - this.panels.css( "height", "" ); - } - }, + //The element's absolute position on the page minus margins + this.offset = this.currentItem.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; - enable: function( index ) { - var disabled = this.options.disabled; - if ( disabled === false ) { - return; - } + $.extend( this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, - if ( index === undefined ) { - disabled = false; - } else { - index = this._getIndex( index ); - if ( Array.isArray( disabled ) ) { - disabled = $.map( disabled, function( num ) { - return num !== index ? num : null; - } ); - } else { - disabled = $.map( this.tabs, function( li, num ) { - return num !== index ? num : null; - } ); - } - } - this._setOptionDisabled( disabled ); - }, + // This is a relative to absolute position minus the actual position calculation - + // only used for relative positioned helper + relative: this._getRelativeOffset() + } ); - disable: function( index ) { - var disabled = this.options.disabled; - if ( disabled === true ) { - return; + // After we get the helper offset, but before we get the parent offset we can + // change the helper's position to absolute + // TODO: Still need to figure out a way to make relative sorting possible + this.helper.css( "position", "absolute" ); + this.cssPosition = this.helper.css( "position" ); + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + if ( o.cursorAt ) { + this._adjustOffsetFromHelper( o.cursorAt ); } - if ( index === undefined ) { - disabled = true; - } else { - index = this._getIndex( index ); - if ( $.inArray( index, disabled ) !== -1 ) { - return; - } - if ( Array.isArray( disabled ) ) { - disabled = $.merge( [ index ], disabled ).sort(); - } else { - disabled = [ index ]; - } + //Cache the former DOM position + this.domPosition = { + prev: this.currentItem.prev()[ 0 ], + parent: this.currentItem.parent()[ 0 ] + }; + + // If the helper is not the original, hide the original so it's not playing any role during + // the drag, won't cause anything bad this way + if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { + this.currentItem.hide(); } - this._setOptionDisabled( disabled ); - }, - load: function( index, event ) { - index = this._getIndex( index ); - var that = this, - tab = this.tabs.eq( index ), - anchor = tab.find( ".ui-tabs-anchor" ), - panel = this._getPanelForTab( tab ), - eventData = { - tab: tab, - panel: panel - }, - complete = function( jqXHR, status ) { - if ( status === "abort" ) { - that.panels.stop( false, true ); - } + //Create the placeholder + this._createPlaceholder(); - that._removeClass( tab, "ui-tabs-loading" ); - panel.removeAttr( "aria-busy" ); + //Get the next scrolling parent + this.scrollParent = this.placeholder.scrollParent(); - if ( jqXHR === that.xhr ) { - delete that.xhr; - } - }; + $.extend( this.offset, { + parent: this._getParentOffset() + } ); - // Not remote - if ( this._isLocal( anchor[ 0 ] ) ) { - return; + //Set a containment if given in the options + if ( o.containment ) { + this._setContainment(); } - this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); - - // Support: jQuery <1.8 - // jQuery <1.8 returns false if the request is canceled in beforeSend, - // but as of 1.8, $.ajax() always returns a jqXHR object. - if ( this.xhr && this.xhr.statusText !== "canceled" ) { - this._addClass( tab, "ui-tabs-loading" ); - panel.attr( "aria-busy", "true" ); + if ( o.cursor && o.cursor !== "auto" ) { // cursor option + body = this.document.find( "body" ); - this.xhr - .done( function( response, status, jqXHR ) { + // Support: IE + this.storedCursor = body.css( "cursor" ); + body.css( "cursor", o.cursor ); - // support: jQuery <1.8 - // http://bugs.jquery.com/ticket/11778 - setTimeout( function() { - panel.html( response ); - that._trigger( "load", event, eventData ); + this.storedStylesheet = + $( "<style>*{ cursor: " + o.cursor + " !important; }</style>" ).appendTo( body ); + } - complete( jqXHR, status ); - }, 1 ); - } ) - .fail( function( jqXHR, status ) { + // We need to make sure to grab the zIndex before setting the + // opacity, because setting the opacity to anything lower than 1 + // causes the zIndex to change from "auto" to 0. + if ( o.zIndex ) { // zIndex option + if ( this.helper.css( "zIndex" ) ) { + this._storedZIndex = this.helper.css( "zIndex" ); + } + this.helper.css( "zIndex", o.zIndex ); + } - // support: jQuery <1.8 - // http://bugs.jquery.com/ticket/11778 - setTimeout( function() { - complete( jqXHR, status ); - }, 1 ); - } ); + if ( o.opacity ) { // opacity option + if ( this.helper.css( "opacity" ) ) { + this._storedOpacity = this.helper.css( "opacity" ); + } + this.helper.css( "opacity", o.opacity ); } - }, - _ajaxSettings: function( anchor, event, eventData ) { - var that = this; - return { + //Prepare scrolling + if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ].tagName !== "HTML" ) { + this.overflowOffset = this.scrollParent.offset(); + } - // Support: IE <11 only - // Strip any hash that exists to prevent errors with the Ajax request - url: anchor.attr( "href" ).replace( /#.*$/, "" ), - beforeSend: function( jqXHR, settings ) { - return that._trigger( "beforeLoad", event, - $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) ); - } - }; - }, + //Call callbacks + this._trigger( "start", event, this._uiHash() ); - _getPanelForTab: function( tab ) { - var id = $( tab ).attr( "aria-controls" ); - return this.element.find( this._sanitizeSelector( "#" + id ) ); - } -} ); + //Recache the helper size + if ( !this._preserveHelperProportions ) { + this._cacheHelperProportions(); + } -// DEPRECATED -// TODO: Switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { + //Post "activate" events to possible containers + if ( !noActivation ) { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); + } + } - // Backcompat for ui-tab class (now ui-tabs-tab) - $.widget( "ui.tabs", $.ui.tabs, { - _processTabs: function() { - this._superApply( arguments ); - this._addClass( this.tabs, "ui-tab" ); + //Prepare possible droppables + if ( $.ui.ddmanager ) { + $.ui.ddmanager.current = this; } - } ); -} -var widgetsTabs = $.ui.tabs; + if ( $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( this, event ); + } + this.dragging = true; -/*! - * jQuery UI Tooltip 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this._addClass( this.helper, "ui-sortable-helper" ); -//>>label: Tooltip -//>>group: Widgets -//>>description: Shows additional information for any element on hover or focus. -//>>docs: http://api.jqueryui.com/tooltip/ -//>>demos: http://jqueryui.com/tooltip/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/tooltip.css -//>>css.theme: ../../themes/base/theme.css + //Move the helper, if needed + if ( !this.helper.parent().is( this.appendTo ) ) { + this.helper.detach().appendTo( this.appendTo ); + //Update position + this.offset.parent = this._getParentOffset(); + } -$.widget( "ui.tooltip", { - version: "1.13.1", - options: { - classes: { - "ui-tooltip": "ui-corner-all ui-widget-shadow" - }, - content: function() { - var title = $( this ).attr( "title" ); + //Generate the original position + this.position = this.originalPosition = this._generatePosition( event ); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + this.lastPositionAbs = this.positionAbs = this._convertPositionTo( "absolute" ); - // Escape title, since we're going from an attribute to raw HTML - return $( "<a>" ).text( title ).html(); - }, - hide: true, + this._mouseDrag( event ); - // Disabled elements have inconsistent behavior across browsers (#8661) - items: "[title]:not([disabled])", - position: { - my: "left top+15", - at: "left bottom", - collision: "flipfit flip" - }, - show: true, - track: false, + return true; - // Callbacks - close: null, - open: null }, - _addDescribedBy: function( elem, id ) { - var describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ); - describedby.push( id ); - elem - .data( "ui-tooltip-id", id ) - .attr( "aria-describedby", String.prototype.trim.call( describedby.join( " " ) ) ); - }, + _scroll: function( event ) { + var o = this.options, + scrolled = false; - _removeDescribedBy: function( elem ) { - var id = elem.data( "ui-tooltip-id" ), - describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ), - index = $.inArray( id, describedby ); + if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ].tagName !== "HTML" ) { - if ( index !== -1 ) { - describedby.splice( index, 1 ); - } + if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) - + event.pageY < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollTop = + scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed; + } else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollTop = + scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed; + } - elem.removeData( "ui-tooltip-id" ); - describedby = String.prototype.trim.call( describedby.join( " " ) ); - if ( describedby ) { - elem.attr( "aria-describedby", describedby ); - } else { - elem.removeAttr( "aria-describedby" ); - } - }, + if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) - + event.pageX < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollLeft = scrolled = + this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed; + } else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollLeft = scrolled = + this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed; + } - _create: function() { - this._on( { - mouseover: "open", - focusin: "open" - } ); + } else { - // IDs of generated tooltips, needed for destroy - this.tooltips = {}; + if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) { + scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed ); + } else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) < + o.scrollSensitivity ) { + scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed ); + } - // IDs of parent tooltips where we removed the title attribute - this.parents = {}; + if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) { + scrolled = this.document.scrollLeft( + this.document.scrollLeft() - o.scrollSpeed + ); + } else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) < + o.scrollSensitivity ) { + scrolled = this.document.scrollLeft( + this.document.scrollLeft() + o.scrollSpeed + ); + } - // Append the aria-live region so tooltips announce correctly - this.liveRegion = $( "<div>" ) - .attr( { - role: "log", - "aria-live": "assertive", - "aria-relevant": "additions" - } ) - .appendTo( this.document[ 0 ].body ); - this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" ); + } - this.disabledTitles = $( [] ); + return scrolled; }, - _setOption: function( key, value ) { - var that = this; + _mouseDrag: function( event ) { + var i, item, itemElement, intersection, + o = this.options; - this._super( key, value ); + //Compute the helpers position + this.position = this._generatePosition( event ); + this.positionAbs = this._convertPositionTo( "absolute" ); - if ( key === "content" ) { - $.each( this.tooltips, function( id, tooltipData ) { - that._updateContent( tooltipData.element ); - } ); + //Set the helper position + if ( !this.options.axis || this.options.axis !== "y" ) { + this.helper[ 0 ].style.left = this.position.left + "px"; + } + if ( !this.options.axis || this.options.axis !== "x" ) { + this.helper[ 0 ].style.top = this.position.top + "px"; } - }, - _setOptionDisabled: function( value ) { - this[ value ? "_disable" : "_enable" ](); - }, + //Do scrolling + if ( o.scroll ) { + if ( this._scroll( event ) !== false ) { - _disable: function() { - var that = this; + //Update item positions used in position checks + this._refreshItemPositions( true ); - // Close open tooltips - $.each( this.tooltips, function( id, tooltipData ) { - var event = $.Event( "blur" ); - event.target = event.currentTarget = tooltipData.element[ 0 ]; - that.close( event, true ); - } ); + if ( $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( this, event ); + } + } + } - // Remove title attributes to prevent native tooltips - this.disabledTitles = this.disabledTitles.add( - this.element.find( this.options.items ).addBack() - .filter( function() { - var element = $( this ); - if ( element.is( "[title]" ) ) { - return element - .data( "ui-tooltip-title", element.attr( "title" ) ) - .removeAttr( "title" ); - } - } ) - ); - }, + this.dragDirection = { + vertical: this._getDragVerticalDirection(), + horizontal: this._getDragHorizontalDirection() + }; - _enable: function() { + //Rearrange + for ( i = this.items.length - 1; i >= 0; i-- ) { - // restore title attributes - this.disabledTitles.each( function() { - var element = $( this ); - if ( element.data( "ui-tooltip-title" ) ) { - element.attr( "title", element.data( "ui-tooltip-title" ) ); + //Cache variables and intersection, continue if no intersection + item = this.items[ i ]; + itemElement = item.item[ 0 ]; + intersection = this._intersectsWithPointer( item ); + if ( !intersection ) { + continue; } - } ); - this.disabledTitles = $( [] ); - }, - open: function( event ) { - var that = this, - target = $( event ? event.target : this.element ) + // Only put the placeholder inside the current Container, skip all + // items from other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this, moving items in "sub-sortables" can cause + // the placeholder to jitter between the outer and inner container. + if ( item.instance !== this.currentContainer ) { + continue; + } + + // Cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if ( itemElement !== this.currentItem[ 0 ] && + this.placeholder[ intersection === 1 ? + "next" : "prev" ]()[ 0 ] !== itemElement && + !$.contains( this.placeholder[ 0 ], itemElement ) && + ( this.options.type === "semi-dynamic" ? + !$.contains( this.element[ 0 ], itemElement ) : + true + ) + ) { + + this.direction = intersection === 1 ? "down" : "up"; - // we need closest here due to mouseover bubbling, - // but always pointing at the same event target - .closest( this.options.items ); + if ( this.options.tolerance === "pointer" || + this._intersectsWithSides( item ) ) { + this._rearrange( event, item ); + } else { + break; + } - // No element to show a tooltip for or the tooltip is already open - if ( !target.length || target.data( "ui-tooltip-id" ) ) { - return; + this._trigger( "change", event, this._uiHash() ); + break; + } } - if ( target.attr( "title" ) ) { - target.data( "ui-tooltip-title", target.attr( "title" ) ); + //Post events to containers + this._contactContainers( event ); + + //Interconnect with droppables + if ( $.ui.ddmanager ) { + $.ui.ddmanager.drag( this, event ); } - target.data( "ui-tooltip-open", true ); + //Call callbacks + this._trigger( "sort", event, this._uiHash() ); - // Kill parent tooltips, custom or native, for hover - if ( event && event.type === "mouseover" ) { - target.parents().each( function() { - var parent = $( this ), - blurEvent; - if ( parent.data( "ui-tooltip-open" ) ) { - blurEvent = $.Event( "blur" ); - blurEvent.target = blurEvent.currentTarget = this; - that.close( blurEvent, true ); - } - if ( parent.attr( "title" ) ) { - parent.uniqueId(); - that.parents[ this.id ] = { - element: this, - title: parent.attr( "title" ) - }; - parent.attr( "title", "" ); - } - } ); - } + this.lastPositionAbs = this.positionAbs; + return false; - this._registerCloseHandlers( event, target ); - this._updateContent( target, event ); }, - _updateContent: function( target, event ) { - var content, - contentOption = this.options.content, - that = this, - eventType = event ? event.type : null; + _mouseStop: function( event, noPropagation ) { - if ( typeof contentOption === "string" || contentOption.nodeType || - contentOption.jquery ) { - return this._open( event, target, contentOption ); + if ( !event ) { + return; } - content = contentOption.call( target[ 0 ], function( response ) { - - // IE may instantly serve a cached response for ajax requests - // delay this call to _open so the other call to _open runs first - that._delay( function() { + //If we are using droppables, inform the manager about the drop + if ( $.ui.ddmanager && !this.options.dropBehaviour ) { + $.ui.ddmanager.drop( this, event ); + } - // Ignore async response if tooltip was closed already - if ( !target.data( "ui-tooltip-open" ) ) { - return; - } + if ( this.options.revert ) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; - // JQuery creates a special event for focusin when it doesn't - // exist natively. To improve performance, the native event - // object is reused and the type is changed. Therefore, we can't - // rely on the type being correct after the event finished - // bubbling, so we set it back to the previous value. (#8740) - if ( event ) { - event.type = eventType; + if ( !axis || axis === "x" ) { + animation.left = cur.left - this.offset.parent.left - this.margins.left + + ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? + 0 : + this.offsetParent[ 0 ].scrollLeft + ); + } + if ( !axis || axis === "y" ) { + animation.top = cur.top - this.offset.parent.top - this.margins.top + + ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? + 0 : + this.offsetParent[ 0 ].scrollTop + ); + } + this.reverting = true; + $( this.helper ).animate( + animation, + parseInt( this.options.revert, 10 ) || 500, + function() { + that._clear( event ); } - this._open( event, target, response ); - } ); - } ); - if ( content ) { - this._open( event, target, content ); + ); + } else { + this._clear( event, noPropagation ); } + + return false; + }, - _open: function( event, target, content ) { - var tooltipData, tooltip, delayedShow, a11yContent, - positionOption = $.extend( {}, this.options.position ); + cancel: function() { - if ( !content ) { - return; - } + if ( this.dragging ) { - // Content can be updated multiple times. If the tooltip already - // exists, then just update the content and bail. - tooltipData = this._find( target ); - if ( tooltipData ) { - tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content ); - return; - } + this._mouseUp( new $.Event( "mouseup", { target: null } ) ); - // If we have a title, clear it to prevent the native tooltip - // we have to check first to avoid defining a title if none exists - // (we don't want to cause an element to start matching [title]) - // - // We use removeAttr only for key events, to allow IE to export the correct - // accessible attributes. For mouse events, set to empty string to avoid - // native tooltip showing up (happens only when removing inside mouseover). - if ( target.is( "[title]" ) ) { - if ( event && event.type === "mouseover" ) { - target.attr( "title", "" ); + if ( this.options.helper === "original" ) { + this.currentItem.css( this._storedCSS ); + this._removeClass( this.currentItem, "ui-sortable-helper" ); } else { - target.removeAttr( "title" ); + this.currentItem.show(); } - } - tooltipData = this._tooltip( target ); - tooltip = tooltipData.tooltip; - this._addDescribedBy( target, tooltip.attr( "id" ) ); - tooltip.find( ".ui-tooltip-content" ).html( content ); + //Post deactivating events to containers + for ( var i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) ); + if ( this.containers[ i ].containerCache.over ) { + this.containers[ i ]._trigger( "out", null, this._uiHash( this ) ); + this.containers[ i ].containerCache.over = 0; + } + } - // Support: Voiceover on OS X, JAWS on IE <= 9 - // JAWS announces deletions even when aria-relevant="additions" - // Voiceover will sometimes re-read the entire log region's contents from the beginning - this.liveRegion.children().hide(); - a11yContent = $( "<div>" ).html( tooltip.find( ".ui-tooltip-content" ).html() ); - a11yContent.removeAttr( "name" ).find( "[name]" ).removeAttr( "name" ); - a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" ); - a11yContent.appendTo( this.liveRegion ); + } - function position( event ) { - positionOption.of = event; - if ( tooltip.is( ":hidden" ) ) { - return; + if ( this.placeholder ) { + + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, + // it unbinds ALL events from the original node! + if ( this.placeholder[ 0 ].parentNode ) { + this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); } - tooltip.position( positionOption ); - } - if ( this.options.track && event && /^mouse/.test( event.type ) ) { - this._on( this.document, { - mousemove: position + if ( this.options.helper !== "original" && this.helper && + this.helper[ 0 ].parentNode ) { + this.helper.remove(); + } + + $.extend( this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null } ); - // trigger once to override element-relative positioning - position( event ); - } else { - tooltip.position( $.extend( { - of: target - }, this.options.position ) ); + if ( this.domPosition.prev ) { + $( this.domPosition.prev ).after( this.currentItem ); + } else { + $( this.domPosition.parent ).prepend( this.currentItem ); + } } - tooltip.hide(); + return this; - this._show( tooltip, this.options.show ); + }, - // Handle tracking tooltips that are shown with a delay (#8644). As soon - // as the tooltip is visible, position the tooltip using the most recent - // event. - // Adds the check to add the timers only when both delay and track options are set (#14682) - if ( this.options.track && this.options.show && this.options.show.delay ) { - delayedShow = this.delayedShow = setInterval( function() { - if ( tooltip.is( ":visible" ) ) { - position( positionOption.of ); - clearInterval( delayedShow ); - } - }, 13 ); - } + serialize: function( o ) { - this._trigger( "open", event, { tooltip: tooltip } ); - }, + var items = this._getItemsAsjQuery( o && o.connected ), + str = []; + o = o || {}; - _registerCloseHandlers: function( event, target ) { - var events = { - keyup: function( event ) { - if ( event.keyCode === $.ui.keyCode.ESCAPE ) { - var fakeEvent = $.Event( event ); - fakeEvent.currentTarget = target[ 0 ]; - this.close( fakeEvent, true ); - } + $( items ).each( function() { + var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" ) + .match( o.expression || ( /(.+)[\-=_](.+)/ ) ); + if ( res ) { + str.push( + ( o.key || res[ 1 ] + "[]" ) + + "=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) ); } - }; + } ); - // Only bind remove handler for delegated targets. Non-delegated - // tooltips will handle this in destroy. - if ( target[ 0 ] !== this.element[ 0 ] ) { - events.remove = function() { - var targetElement = this._find( target ); - if ( targetElement ) { - this._removeTooltip( targetElement.tooltip ); - } - }; + if ( !str.length && o.key ) { + str.push( o.key + "=" ); } - if ( !event || event.type === "mouseover" ) { - events.mouseleave = "close"; - } - if ( !event || event.type === "focusin" ) { - events.focusout = "close"; - } - this._on( true, target, events ); - }, + return str.join( "&" ); - close: function( event ) { - var tooltip, - that = this, - target = $( event ? event.currentTarget : this.element ), - tooltipData = this._find( target ); + }, - // The tooltip may already be closed - if ( !tooltipData ) { + toArray: function( o ) { - // We set ui-tooltip-open immediately upon open (in open()), but only set the - // additional data once there's actually content to show (in _open()). So even if the - // tooltip doesn't have full data, we always remove ui-tooltip-open in case we're in - // the period between open() and _open(). - target.removeData( "ui-tooltip-open" ); - return; - } + var items = this._getItemsAsjQuery( o && o.connected ), + ret = []; - tooltip = tooltipData.tooltip; + o = o || {}; - // Disabling closes the tooltip, so we need to track when we're closing - // to avoid an infinite loop in case the tooltip becomes disabled on close - if ( tooltipData.closing ) { - return; - } + items.each( function() { + ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" ); + } ); + return ret; - // Clear the interval for delayed tracking tooltips - clearInterval( this.delayedShow ); + }, - // Only set title if we had one before (see comment in _open()) - // If the title attribute has changed since open(), don't restore - if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) { - target.attr( "title", target.data( "ui-tooltip-title" ) ); - } + /* Be careful with the following core functions */ + _intersectsWith: function( item ) { - this._removeDescribedBy( target ); + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && + ( y1 + dyClick ) < b ), + isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && + ( x1 + dxClick ) < r ), + isOverElement = isOverElementHeight && isOverElementWidth; - tooltipData.hiding = true; - tooltip.stop( true ); - this._hide( tooltip, this.options.hide, function() { - that._removeTooltip( $( this ) ); - } ); + if ( this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + ( this.options.tolerance !== "pointer" && + this.helperProportions[ this.floating ? "width" : "height" ] > + item[ this.floating ? "width" : "height" ] ) + ) { + return isOverElement; + } else { - target.removeData( "ui-tooltip-open" ); - this._off( target, "mouseleave focusout keyup" ); + return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half + x2 - ( this.helperProportions.width / 2 ) < r && // Left Half + t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half + y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half - // Remove 'remove' binding only on delegated targets - if ( target[ 0 ] !== this.element[ 0 ] ) { - this._off( target, "remove" ); } - this._off( this.document, "mousemove" ); + }, - if ( event && event.type === "mouseleave" ) { - $.each( this.parents, function( id, parent ) { - $( parent.element ).attr( "title", parent.title ); - delete that.parents[ id ]; - } ); - } + _intersectsWithPointer: function( item ) { + var verticalDirection, horizontalDirection, + isOverElementHeight = ( this.options.axis === "x" ) || + this._isOverAxis( + this.positionAbs.top + this.offset.click.top, item.top, item.height ), + isOverElementWidth = ( this.options.axis === "y" ) || + this._isOverAxis( + this.positionAbs.left + this.offset.click.left, item.left, item.width ), + isOverElement = isOverElementHeight && isOverElementWidth; - tooltipData.closing = true; - this._trigger( "close", event, { tooltip: tooltip } ); - if ( !tooltipData.hiding ) { - tooltipData.closing = false; + if ( !isOverElement ) { + return false; } + + verticalDirection = this.dragDirection.vertical; + horizontalDirection = this.dragDirection.horizontal; + + return this.floating ? + ( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) : + ( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) ); + }, - _tooltip: function( element ) { - var tooltip = $( "<div>" ).attr( "role", "tooltip" ), - content = $( "<div>" ).appendTo( tooltip ), - id = tooltip.uniqueId().attr( "id" ); + _intersectsWithSides: function( item ) { - this._addClass( content, "ui-tooltip-content" ); - this._addClass( tooltip, "ui-tooltip", "ui-widget ui-widget-content" ); + var isOverBottomHalf = this._isOverAxis( this.positionAbs.top + + this.offset.click.top, item.top + ( item.height / 2 ), item.height ), + isOverRightHalf = this._isOverAxis( this.positionAbs.left + + this.offset.click.left, item.left + ( item.width / 2 ), item.width ), + verticalDirection = this.dragDirection.vertical, + horizontalDirection = this.dragDirection.horizontal; - tooltip.appendTo( this._appendTo( element ) ); + if ( this.floating && horizontalDirection ) { + return ( ( horizontalDirection === "right" && isOverRightHalf ) || + ( horizontalDirection === "left" && !isOverRightHalf ) ); + } else { + return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) || + ( verticalDirection === "up" && !isOverBottomHalf ) ); + } - return this.tooltips[ id ] = { - element: element, - tooltip: tooltip - }; }, - _find: function( target ) { - var id = target.data( "ui-tooltip-id" ); - return id ? this.tooltips[ id ] : null; + _getDragVerticalDirection: function() { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && ( delta > 0 ? "down" : "up" ); }, - _removeTooltip: function( tooltip ) { + _getDragHorizontalDirection: function() { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && ( delta > 0 ? "right" : "left" ); + }, - // Clear the interval for delayed tracking tooltips - clearInterval( this.delayedShow ); + refresh: function( event ) { + this._refreshItems( event ); + this._setHandleClassName(); + this.refreshPositions(); + return this; + }, - tooltip.remove(); - delete this.tooltips[ tooltip.attr( "id" ) ]; + _connectWith: function() { + var options = this.options; + return options.connectWith.constructor === String ? + [ options.connectWith ] : + options.connectWith; }, - _appendTo: function( target ) { - var element = target.closest( ".ui-front, dialog" ); + _getItemsAsjQuery: function( connected ) { - if ( !element.length ) { - element = this.document[ 0 ].body; + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if ( connectWith && connected ) { + for ( i = connectWith.length - 1; i >= 0; i-- ) { + cur = $( connectWith[ i ], this.document[ 0 ] ); + for ( j = cur.length - 1; j >= 0; j-- ) { + inst = $.data( cur[ j ], this.widgetFullName ); + if ( inst && inst !== this && !inst.options.disabled ) { + queries.push( [ typeof inst.options.items === "function" ? + inst.options.items.call( inst.element ) : + $( inst.options.items, inst.element ) + .not( ".ui-sortable-helper" ) + .not( ".ui-sortable-placeholder" ), inst ] ); + } + } + } } - return element; - }, + queries.push( [ typeof this.options.items === "function" ? + this.options.items + .call( this.element, null, { options: this.options, item: this.currentItem } ) : + $( this.options.items, this.element ) + .not( ".ui-sortable-helper" ) + .not( ".ui-sortable-placeholder" ), this ] ); - _destroy: function() { - var that = this; + function addItems() { + items.push( this ); + } + for ( i = queries.length - 1; i >= 0; i-- ) { + queries[ i ][ 0 ].each( addItems ); + } - // Close open tooltips - $.each( this.tooltips, function( id, tooltipData ) { + return $( items ); - // Delegate to close method to handle common cleanup - var event = $.Event( "blur" ), - element = tooltipData.element; - event.target = event.currentTarget = element[ 0 ]; - that.close( event, true ); + }, - // Remove immediately; destroying an open tooltip doesn't use the - // hide animation - $( "#" + id ).remove(); + _removeCurrentsFromItems: function() { - // Restore the title - if ( element.data( "ui-tooltip-title" ) ) { + var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" ); - // If the title attribute has changed since open(), don't restore - if ( !element.attr( "title" ) ) { - element.attr( "title", element.data( "ui-tooltip-title" ) ); + this.items = $.grep( this.items, function( item ) { + for ( var j = 0; j < list.length; j++ ) { + if ( list[ j ] === item.item[ 0 ] ) { + return false; } - element.removeData( "ui-tooltip-title" ); - } - } ); - this.liveRegion.remove(); - } -} ); - -// DEPRECATED -// TODO: Switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { - - // Backcompat for tooltipClass option - $.widget( "ui.tooltip", $.ui.tooltip, { - options: { - tooltipClass: null - }, - _tooltip: function() { - var tooltipData = this._superApply( arguments ); - if ( this.options.tooltipClass ) { - tooltipData.tooltip.addClass( this.options.tooltipClass ); } - return tooltipData; - } - } ); -} + return true; + } ); -var widgetsTooltip = $.ui.tooltip; + }, + _refreshItems: function( event ) { + this.items = []; + this.containers = [ this ]; -// Create a local jQuery because jQuery Color relies on it and the -// global may not exist with AMD and a custom build (#10199). -// This module is a noop if used as a regular AMD module. -// eslint-disable-next-line no-unused-vars -var jQuery = $; + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [ [ typeof this.options.items === "function" ? + this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) : + $( this.options.items, this.element ), this ] ], + connectWith = this._connectWith(); + //Shouldn't be run the first time through due to massive slow-down + if ( connectWith && this.ready ) { + for ( i = connectWith.length - 1; i >= 0; i-- ) { + cur = $( connectWith[ i ], this.document[ 0 ] ); + for ( j = cur.length - 1; j >= 0; j-- ) { + inst = $.data( cur[ j ], this.widgetFullName ); + if ( inst && inst !== this && !inst.options.disabled ) { + queries.push( [ typeof inst.options.items === "function" ? + inst.options.items + .call( inst.element[ 0 ], event, { item: this.currentItem } ) : + $( inst.options.items, inst.element ), inst ] ); + this.containers.push( inst ); + } + } + } + } -/*! - * jQuery Color Animations v2.2.0 - * https://github.com/jquery/jquery-color - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * Date: Sun May 10 09:02:36 2020 +0200 - */ + for ( i = queries.length - 1; i >= 0; i-- ) { + targetData = queries[ i ][ 1 ]; + _queries = queries[ i ][ 0 ]; + for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) { + item = $( _queries[ j ] ); + // Data for target checking (mouse manager) + item.data( this.widgetName + "-item", targetData ); - var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " + - "borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", + items.push( { + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + } ); + } + } - class2type = {}, - toString = class2type.toString, + }, - // plusequals test for += 100 -= 100 - rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, + _refreshItemPositions: function( fast ) { + var i, item, t, p; - // a set of RE's that can match strings and generate color tuples. - stringParsers = [ { - re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - execResult[ 1 ], - execResult[ 2 ], - execResult[ 3 ], - execResult[ 4 ] - ]; - } - }, { - re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - execResult[ 1 ] * 2.55, - execResult[ 2 ] * 2.55, - execResult[ 3 ] * 2.55, - execResult[ 4 ] - ]; - } - }, { + for ( i = this.items.length - 1; i >= 0; i-- ) { + item = this.items[ i ]; - // this regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ], 16 ), - execResult[ 4 ] ? - ( parseInt( execResult[ 4 ], 16 ) / 255 ).toFixed( 2 ) : - 1 - ]; + //We ignore calculating positions of all connected containers when we're not over them + if ( this.currentContainer && item.instance !== this.currentContainer && + item.item[ 0 ] !== this.currentItem[ 0 ] ) { + continue; } - }, { - // this regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ), - execResult[ 4 ] ? - ( parseInt( execResult[ 4 ] + execResult[ 4 ], 16 ) / 255 ) - .toFixed( 2 ) : - 1 - ]; - } - }, { - re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, - space: "hsla", - parse: function( execResult ) { - return [ - execResult[ 1 ], - execResult[ 2 ] / 100, - execResult[ 3 ] / 100, - execResult[ 4 ] - ]; - } - } ], + t = this.options.toleranceElement ? + $( this.options.toleranceElement, item.item ) : + item.item; - // jQuery.Color( ) - color = jQuery.Color = function( color, green, blue, alpha ) { - return new jQuery.Color.fn.parse( color, green, blue, alpha ); - }, - spaces = { - rgba: { - props: { - red: { - idx: 0, - type: "byte" - }, - green: { - idx: 1, - type: "byte" - }, - blue: { - idx: 2, - type: "byte" - } + if ( !fast ) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); } - }, - hsla: { - props: { - hue: { - idx: 0, - type: "degrees" - }, - saturation: { - idx: 1, - type: "percent" - }, - lightness: { - idx: 2, - type: "percent" - } - } + p = t.offset(); + item.left = p.left; + item.top = p.top; } }, - propTypes = { - "byte": { - floor: true, - max: 255 - }, - "percent": { - max: 1 - }, - "degrees": { - mod: 360, - floor: true + + refreshPositions: function( fast ) { + + // Determine whether items are being displayed horizontally + this.floating = this.items.length ? + this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) : + false; + + // This has to be redone because due to the item being moved out/into the offsetParent, + // the offsetParent's position will change + if ( this.offsetParent && this.helper ) { + this.offset.parent = this._getParentOffset(); } - }, - support = color.support = {}, - // element for support tests - supportElem = jQuery( "<p>" )[ 0 ], + this._refreshItemPositions( fast ); - // colors = jQuery.Color.names - colors, + var i, p; - // local aliases of functions called often - each = jQuery.each; + if ( this.options.custom && this.options.custom.refreshContainers ) { + this.options.custom.refreshContainers.call( this ); + } else { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + p = this.containers[ i ].element.offset(); + this.containers[ i ].containerCache.left = p.left; + this.containers[ i ].containerCache.top = p.top; + this.containers[ i ].containerCache.width = + this.containers[ i ].element.outerWidth(); + this.containers[ i ].containerCache.height = + this.containers[ i ].element.outerHeight(); + } + } -// determine rgba support immediately -supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; -support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; + return this; + }, -// define cache name and alpha properties -// for rgba and hsla spaces -each( spaces, function( spaceName, space ) { - space.cache = "_" + spaceName; - space.props.alpha = { - idx: 3, - type: "percent", - def: 1 - }; -} ); + _createPlaceholder: function( that ) { + that = that || this; + var className, nodeName, + o = that.options; -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), - function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); - } ); + if ( !o.placeholder || o.placeholder.constructor === String ) { + className = o.placeholder; + nodeName = that.currentItem[ 0 ].nodeName.toLowerCase(); + o.placeholder = { + element: function() { + + var element = $( "<" + nodeName + ">", that.document[ 0 ] ); + + that._addClass( element, "ui-sortable-placeholder", + className || that.currentItem[ 0 ].className ) + ._removeClass( element, "ui-sortable-helper" ); -function getType( obj ) { - if ( obj == null ) { - return obj + ""; - } + if ( nodeName === "tbody" ) { + that._createTrPlaceholder( + that.currentItem.find( "tr" ).eq( 0 ), + $( "<tr>", that.document[ 0 ] ).appendTo( element ) + ); + } else if ( nodeName === "tr" ) { + that._createTrPlaceholder( that.currentItem, element ); + } else if ( nodeName === "img" ) { + element.attr( "src", that.currentItem.attr( "src" ) ); + } - return typeof obj === "object" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} + if ( !className ) { + element.css( "visibility", "hidden" ); + } -function clamp( value, prop, allowEmpty ) { - var type = propTypes[ prop.type ] || {}; + return element; + }, + update: function( container, p ) { - if ( value == null ) { - return ( allowEmpty || !prop.def ) ? null : prop.def; - } + // 1. If a className is set as 'placeholder option, we don't force sizes - + // the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a + // class name is specified + if ( className && !o.forcePlaceholderSize ) { + return; + } - // ~~ is an short way of doing floor for positive numbers - value = type.floor ? ~~value : parseFloat( value ); + // If the element doesn't have a actual height or width by itself (without + // styles coming from a stylesheet), it receives the inline height and width + // from the dragged item. Or, if it's a tbody or tr, it's going to have a height + // anyway since we're populating them with <td>s above, but they're unlikely to + // be the correct height on their own if the row heights are dynamic, so we'll + // always assign the height of the dragged item given forcePlaceholderSize + // is true. + if ( !p.height() || ( o.forcePlaceholderSize && + ( nodeName === "tbody" || nodeName === "tr" ) ) ) { + p.height( + that.currentItem.innerHeight() - + parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) - + parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) ); + } + if ( !p.width() ) { + p.width( + that.currentItem.innerWidth() - + parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) - + parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) ); + } + } + }; + } - // IE will pass in empty strings as value for alpha, - // which will hit this case - if ( isNaN( value ) ) { - return prop.def; - } + //Create the placeholder + that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) ); - if ( type.mod ) { + //Append it after the actual current item + that.currentItem.after( that.placeholder ); - // we add mod before modding to make sure that negatives values - // get converted properly: -10 -> 350 - return ( value + type.mod ) % type.mod; - } + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update( that, that.placeholder ); - // for now all property types without mod have min and max - return Math.min( type.max, Math.max( 0, value ) ); -} + }, -function stringParse( string ) { - var inst = color(), - rgba = inst._rgba = []; + _createTrPlaceholder: function( sourceTr, targetTr ) { + var that = this; - string = string.toLowerCase(); + sourceTr.children().each( function() { + $( "<td> </td>", that.document[ 0 ] ) + .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) + .appendTo( targetTr ); + } ); + }, - each( stringParsers, function( _i, parser ) { - var parsed, - match = parser.re.exec( string ), - values = match && parser.parse( match ), - spaceName = parser.space || "rgba"; + _contactContainers: function( event ) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom, + floating, axis, + innermostContainer = null, + innermostIndex = null; - if ( values ) { - parsed = inst[ spaceName ]( values ); + // Get innermost container that intersects with item + for ( i = this.containers.length - 1; i >= 0; i-- ) { - // if this was an rgba parse the assignment might happen twice - // oh well.... - inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; - rgba = inst._rgba = parsed._rgba; + // Never consider a container that's located within the item itself + if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) { + continue; + } - // exit each( stringParsers ) here because we matched - return false; - } - } ); + if ( this._intersectsWith( this.containers[ i ].containerCache ) ) { - // Found a stringParser that handled it - if ( rgba.length ) { + // If we've already found a container and it's more "inner" than this, then continue + if ( innermostContainer && + $.contains( + this.containers[ i ].element[ 0 ], + innermostContainer.element[ 0 ] ) ) { + continue; + } - // if this came from a parsed string, force "transparent" when alpha is 0 - // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) - if ( rgba.join() === "0,0,0,0" ) { - jQuery.extend( rgba, colors.transparent ); - } - return inst; - } + innermostContainer = this.containers[ i ]; + innermostIndex = i; - // named colors - return colors[ string ]; -} + } else { + + // container doesn't intersect. trigger "out" event if necessary + if ( this.containers[ i ].containerCache.over ) { + this.containers[ i ]._trigger( "out", event, this._uiHash( this ) ); + this.containers[ i ].containerCache.over = 0; + } + } -color.fn = jQuery.extend( color.prototype, { - parse: function( red, green, blue, alpha ) { - if ( red === undefined ) { - this._rgba = [ null, null, null, null ]; - return this; } - if ( red.jquery || red.nodeType ) { - red = jQuery( red ).css( green ); - green = undefined; + + // If no intersecting containers found, return + if ( !innermostContainer ) { + return; } - var inst = this, - type = getType( red ), - rgba = this._rgba = []; + // Move the item into the container if it's not there already + if ( this.containers.length === 1 ) { + if ( !this.containers[ innermostIndex ].containerCache.over ) { + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); + this.containers[ innermostIndex ].containerCache.over = 1; + } + } else { - // more than 1 argument specified - assume ( red, green, blue, alpha ) - if ( green !== undefined ) { - red = [ red, green, blue, alpha ]; - type = "array"; - } + // When entering a new container, we will find the item with the least distance and + // append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || this._isFloating( this.currentItem ); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + axis = floating ? "pageX" : "pageY"; - if ( type === "string" ) { - return this.parse( stringParse( red ) || colors._default ); - } + for ( j = this.items.length - 1; j >= 0; j-- ) { + if ( !$.contains( + this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] ) + ) { + continue; + } + if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) { + continue; + } - if ( type === "array" ) { - each( spaces.rgba.props, function( _key, prop ) { - rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); - } ); - return this; - } + cur = this.items[ j ].item.offset()[ posProperty ]; + nearBottom = false; + if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) { + nearBottom = true; + } - if ( type === "object" ) { - if ( red instanceof color ) { - each( spaces, function( _spaceName, space ) { - if ( red[ space.cache ] ) { - inst[ space.cache ] = red[ space.cache ].slice(); - } - } ); - } else { - each( spaces, function( _spaceName, space ) { - var cache = space.cache; - each( space.props, function( key, prop ) { + if ( Math.abs( event[ axis ] - cur ) < dist ) { + dist = Math.abs( event[ axis ] - cur ); + itemWithLeastDistance = this.items[ j ]; + this.direction = nearBottom ? "up" : "down"; + } + } - // if the cache doesn't exist, and we know how to convert - if ( !inst[ cache ] && space.to ) { + //Check if dropOnEmpty is enabled + if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) { + return; + } - // if the value was null, we don't need to copy it - // if the key was alpha, we don't need to copy it either - if ( key === "alpha" || red[ key ] == null ) { - return; - } - inst[ cache ] = space.to( inst._rgba ); - } + if ( this.currentContainer === this.containers[ innermostIndex ] ) { + if ( !this.currentContainer.containerCache.over ) { + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() ); + this.currentContainer.containerCache.over = 1; + } + return; + } - // this is the only case where we allow nulls for ALL properties. - // call clamp with alwaysAllowEmpty - inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); - } ); + if ( itemWithLeastDistance ) { + this._rearrange( event, itemWithLeastDistance, null, true ); + } else { + this._rearrange( event, null, this.containers[ innermostIndex ].element, true ); + } + this._trigger( "change", event, this._uiHash() ); + this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) ); + this.currentContainer = this.containers[ innermostIndex ]; - // everything defined but alpha? - if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + //Update the placeholder + this.options.placeholder.update( this.currentContainer, this.placeholder ); - // use the default of 1 - if ( inst[ cache ][ 3 ] == null ) { - inst[ cache ][ 3 ] = 1; - } + //Update scrollParent + this.scrollParent = this.placeholder.scrollParent(); - if ( space.from ) { - inst._rgba = space.from( inst[ cache ] ); - } - } - } ); + //Update overflowOffset + if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ].tagName !== "HTML" ) { + this.overflowOffset = this.scrollParent.offset(); } - return this; + + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); + this.containers[ innermostIndex ].containerCache.over = 1; } - }, - is: function( compare ) { - var is = color( compare ), - same = true, - inst = this; - each( spaces, function( _, space ) { - var localCache, - isCache = is[ space.cache ]; - if ( isCache ) { - localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; - each( space.props, function( _, prop ) { - if ( isCache[ prop.idx ] != null ) { - same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); - return same; - } - } ); - } - return same; - } ); - return same; - }, - _space: function() { - var used = [], - inst = this; - each( spaces, function( spaceName, space ) { - if ( inst[ space.cache ] ) { - used.push( spaceName ); - } - } ); - return used.pop(); }, - transition: function( other, distance ) { - var end = color( other ), - spaceName = end._space(), - space = spaces[ spaceName ], - startColor = this.alpha() === 0 ? color( "transparent" ) : this, - start = startColor[ space.cache ] || space.to( startColor._rgba ), - result = start.slice(); - end = end[ space.cache ]; - each( space.props, function( _key, prop ) { - var index = prop.idx, - startValue = start[ index ], - endValue = end[ index ], - type = propTypes[ prop.type ] || {}; + _createHelper: function( event ) { - // if null, don't override start value - if ( endValue === null ) { - return; - } + var o = this.options, + helper = typeof o.helper === "function" ? + $( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) : + ( o.helper === "clone" ? this.currentItem.clone() : this.currentItem ); - // if null - use end - if ( startValue === null ) { - result[ index ] = endValue; - } else { - if ( type.mod ) { - if ( endValue - startValue > type.mod / 2 ) { - startValue += type.mod; - } else if ( startValue - endValue > type.mod / 2 ) { - startValue -= type.mod; - } - } - result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); - } - } ); - return this[ spaceName ]( result ); - }, - blend: function( opaque ) { + //Add the helper to the DOM if that didn't happen already + if ( !helper.parents( "body" ).length ) { + this.appendTo[ 0 ].appendChild( helper[ 0 ] ); + } - // if we are already opaque - return ourself - if ( this._rgba[ 3 ] === 1 ) { - return this; + if ( helper[ 0 ] === this.currentItem[ 0 ] ) { + this._storedCSS = { + width: this.currentItem[ 0 ].style.width, + height: this.currentItem[ 0 ].style.height, + position: this.currentItem.css( "position" ), + top: this.currentItem.css( "top" ), + left: this.currentItem.css( "left" ) + }; } - var rgb = this._rgba.slice(), - a = rgb.pop(), - blend = color( opaque )._rgba; + if ( !helper[ 0 ].style.width || o.forceHelperSize ) { + helper.width( this.currentItem.width() ); + } + if ( !helper[ 0 ].style.height || o.forceHelperSize ) { + helper.height( this.currentItem.height() ); + } + + return helper; - return color( jQuery.map( rgb, function( v, i ) { - return ( 1 - a ) * blend[ i ] + a * v; - } ) ); }, - toRgbaString: function() { - var prefix = "rgba(", - rgba = jQuery.map( this._rgba, function( v, i ) { - if ( v != null ) { - return v; - } - return i > 2 ? 1 : 0; - } ); - if ( rgba[ 3 ] === 1 ) { - rgba.pop(); - prefix = "rgb("; + _adjustOffsetFromHelper: function( obj ) { + if ( typeof obj === "string" ) { + obj = obj.split( " " ); + } + if ( Array.isArray( obj ) ) { + obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; + } + if ( "left" in obj ) { + this.offset.click.left = obj.left + this.margins.left; + } + if ( "right" in obj ) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ( "top" in obj ) { + this.offset.click.top = obj.top + this.margins.top; + } + if ( "bottom" in obj ) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; } - - return prefix + rgba.join() + ")"; }, - toHslaString: function() { - var prefix = "hsla(", - hsla = jQuery.map( this.hsla(), function( v, i ) { - if ( v == null ) { - v = i > 2 ? 1 : 0; - } - // catch 1 and 2 - if ( i && i < 3 ) { - v = Math.round( v * 100 ) + "%"; - } - return v; - } ); + _getParentOffset: function() { - if ( hsla[ 3 ] === 1 ) { - hsla.pop(); - prefix = "hsl("; + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the + // following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the + // next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't + // the document, which means that the scroll is included in the initial calculation of the + // offset of the parent, and never recalculated upon drag + if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); } - return prefix + hsla.join() + ")"; + + // This needs to be actually done for all browsers, since pageX/pageY includes this + // information with an ugly IE fix + if ( this.offsetParent[ 0 ] === this.document[ 0 ].body || + ( this.offsetParent[ 0 ].tagName && + this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), + left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) + }; + }, - toHexString: function( includeAlpha ) { - var rgba = this._rgba.slice(), - alpha = rgba.pop(); - if ( includeAlpha ) { - rgba.push( ~~( alpha * 255 ) ); + _getRelativeOffset: function() { + + if ( this.cssPosition === "relative" ) { + var p = this.currentItem.position(); + return { + top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + + this.scrollParent.scrollTop(), + left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; } - return "#" + jQuery.map( rgba, function( v ) { + }, - // default to 0 when nulls exist - v = ( v || 0 ).toString( 16 ); - return v.length === 1 ? "0" + v : v; - } ).join( "" ); + _cacheMargins: function() { + this.margins = { + left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ), + top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 ) + }; }, - toString: function() { - return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); - } -} ); -color.fn.parse.prototype = color.fn; -// hsla conversions adapted from: -// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var ce, co, over, + o = this.options; + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; + } + if ( o.containment === "document" || o.containment === "window" ) { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + o.containment === "document" ? + this.document.width() : + this.window.width() - this.helperProportions.width - this.margins.left, + ( o.containment === "document" ? + ( this.document.height() || document.body.parentNode.scrollHeight ) : + this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight + ) - this.helperProportions.height - this.margins.top + ]; + } + + if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) { + ce = $( o.containment )[ 0 ]; + co = $( o.containment ).offset(); + over = ( $( ce ).css( "overflow" ) !== "hidden" ); -function hue2rgb( p, q, h ) { - h = ( h + 1 ) % 1; - if ( h * 6 < 1 ) { - return p + ( q - p ) * h * 6; - } - if ( h * 2 < 1 ) { - return q; - } - if ( h * 3 < 2 ) { - return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6; - } - return p; -} + this.containment = [ + co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) + + ( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left, + co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) + + ( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top, + co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) - + ( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) - + this.helperProportions.width - this.margins.left, + co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) - + ( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) - + this.helperProportions.height - this.margins.top + ]; + } -spaces.hsla.to = function( rgba ) { - if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { - return [ null, null, null, rgba[ 3 ] ]; - } - var r = rgba[ 0 ] / 255, - g = rgba[ 1 ] / 255, - b = rgba[ 2 ] / 255, - a = rgba[ 3 ], - max = Math.max( r, g, b ), - min = Math.min( r, g, b ), - diff = max - min, - add = max + min, - l = add * 0.5, - h, s; + }, - if ( min === max ) { - h = 0; - } else if ( r === max ) { - h = ( 60 * ( g - b ) / diff ) + 360; - } else if ( g === max ) { - h = ( 60 * ( b - r ) / diff ) + 120; - } else { - h = ( 60 * ( r - g ) / diff ) + 240; - } + _convertPositionTo: function( d, pos ) { - // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% - // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) - if ( diff === 0 ) { - s = 0; - } else if ( l <= 0.5 ) { - s = diff / add; - } else { - s = diff / ( 2 - add ); - } - return [ Math.round( h ) % 360, s, l, a == null ? 1 : a ]; -}; + if ( !pos ) { + pos = this.position; + } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && + !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? + this.offsetParent : + this.scrollParent, + scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); -spaces.hsla.from = function( hsla ) { - if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { - return [ null, null, null, hsla[ 3 ] ]; - } - var h = hsla[ 0 ] / 360, - s = hsla[ 1 ], - l = hsla[ 2 ], - a = hsla[ 3 ], - q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, - p = 2 * l - q; + return { + top: ( - return [ - Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), - Math.round( hue2rgb( p, q, h ) * 255 ), - Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), - a - ]; -}; + // The absolute mouse position + pos.top + + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top * mod + -each( spaces, function( spaceName, space ) { - var props = space.props, - cache = space.cache, - to = space.to, - from = space.from; + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top * mod - + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollTop() : + ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod ) + ), + left: ( - // makes rgba() and hsla() - color.fn[ spaceName ] = function( value ) { + // The absolute mouse position + pos.left + - // generate a cache for this space if it doesn't exist - if ( to && !this[ cache ] ) { - this[ cache ] = to( this._rgba ); - } - if ( value === undefined ) { - return this[ cache ].slice(); - } + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left * mod + - var ret, - type = getType( value ), - arr = ( type === "array" || type === "object" ) ? value : arguments, - local = this[ cache ].slice(); + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left * mod - + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : + scroll.scrollLeft() ) * mod ) + ) + }; - each( props, function( key, prop ) { - var val = arr[ type === "object" ? key : prop.idx ]; - if ( val == null ) { - val = local[ prop.idx ]; - } - local[ prop.idx ] = clamp( val, prop ); - } ); + }, - if ( from ) { - ret = color( from( local ) ); - ret[ cache ] = local; - return ret; - } else { - return color( local ); - } - }; + _generatePosition: function( event ) { - // makes red() green() blue() alpha() hue() saturation() lightness() - each( props, function( key, prop ) { + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && + !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? + this.offsetParent : + this.scrollParent, + scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); - // alpha is included in more than one space - if ( color.fn[ key ] ) { - return; + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) { + this.offset.relative = this._getRelativeOffset(); } - color.fn[ key ] = function( value ) { - var local, cur, match, fn, - vtype = getType( value ); - if ( key === "alpha" ) { - fn = this._hsla ? "hsla" : "rgba"; - } else { - fn = spaceName; - } - local = this[ fn ](); - cur = local[ prop.idx ]; + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ - if ( vtype === "undefined" ) { - return cur; - } + if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options - if ( vtype === "function" ) { - value = value.call( this, cur ); - vtype = getType( value ); - } - if ( value == null && prop.empty ) { - return this; - } - if ( vtype === "string" ) { - match = rplusequals.exec( value ); - if ( match ) { - value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); + if ( this.containment ) { + if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) { + pageX = this.containment[ 0 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) { + pageY = this.containment[ 1 ] + this.offset.click.top; + } + if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) { + pageX = this.containment[ 2 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) { + pageY = this.containment[ 3 ] + this.offset.click.top; } } - local[ prop.idx ] = value; - return this[ fn ]( local ); - }; - } ); -} ); -// add cssHook and .fx.step function for each named hook. -// accept a space separated string of properties -color.hook = function( hook ) { - var hooks = hook.split( " " ); - each( hooks, function( _i, hook ) { - jQuery.cssHooks[ hook ] = { - set: function( elem, value ) { - var parsed, curElem, - backgroundColor = ""; + if ( o.grid ) { + top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) / + o.grid[ 1 ] ) * o.grid[ 1 ]; + pageY = this.containment ? + ( ( top - this.offset.click.top >= this.containment[ 1 ] && + top - this.offset.click.top <= this.containment[ 3 ] ) ? + top : + ( ( top - this.offset.click.top >= this.containment[ 1 ] ) ? + top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : + top; + + left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) / + o.grid[ 0 ] ) * o.grid[ 0 ]; + pageX = this.containment ? + ( ( left - this.offset.click.left >= this.containment[ 0 ] && + left - this.offset.click.left <= this.containment[ 2 ] ) ? + left : + ( ( left - this.offset.click.left >= this.containment[ 0 ] ) ? + left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : + left; + } - if ( value !== "transparent" && ( getType( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { - value = color( parsed || value ); - if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { - curElem = hook === "backgroundColor" ? elem.parentNode : elem; - while ( - ( backgroundColor === "" || backgroundColor === "transparent" ) && - curElem && curElem.style - ) { - try { - backgroundColor = jQuery.css( curElem, "backgroundColor" ); - curElem = curElem.parentNode; - } catch ( e ) { - } - } + } - value = value.blend( backgroundColor && backgroundColor !== "transparent" ? - backgroundColor : - "_default" ); - } + return { + top: ( - value = value.toRgbaString(); - } - try { - elem.style[ hook ] = value; - } catch ( e ) { + // The absolute mouse position + pageY - - // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' - } - } - }; - jQuery.fx.step[ hook ] = function( fx ) { - if ( !fx.colorInit ) { - fx.start = color( fx.elem, hook ); - fx.end = color( fx.end ); - fx.colorInit = true; - } - jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); - }; - } ); + // Click offset (relative to the element) + this.offset.click.top - -}; + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top - -color.hook( stepHooks ); + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top + + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollTop() : + ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) ) + ), + left: ( -jQuery.cssHooks.borderColor = { - expand: function( value ) { - var expanded = {}; + // The absolute mouse position + pageX - - each( [ "Top", "Right", "Bottom", "Left" ], function( _i, part ) { - expanded[ "border" + part + "Color" ] = value; - } ); - return expanded; - } -}; + // Click offset (relative to the element) + this.offset.click.left - -// Basic color names only. -// Usage of any of the other color names requires adding yourself or including -// jquery.color.svg-names.js. -colors = jQuery.Color.names = { + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left - - // 4.1. Basic color keywords - aqua: "#00ffff", - black: "#000000", - blue: "#0000ff", - fuchsia: "#ff00ff", - gray: "#808080", - green: "#008000", - lime: "#00ff00", - maroon: "#800000", - navy: "#000080", - olive: "#808000", - purple: "#800080", - red: "#ff0000", - silver: "#c0c0c0", - teal: "#008080", - white: "#ffffff", - yellow: "#ffff00", + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left + + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollLeft() : + scrollIsRootNode ? 0 : scroll.scrollLeft() ) ) + ) + }; - // 4.2.3. "transparent" color keyword - transparent: [ null, null, null, 0 ], + }, - _default: "#ffffff" -}; + _rearrange: function( event, i, a, hardRefresh ) { + if ( a ) { + a[ 0 ].appendChild( this.placeholder[ 0 ] ); + } else { + i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ], + ( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) ); + } -/*! - * jQuery UI Effects 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, + // if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; -//>>label: Effects Core -//>>group: Effects -/* eslint-disable max-len */ -//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. -/* eslint-enable max-len */ -//>>docs: http://api.jqueryui.com/category/effects-core/ -//>>demos: http://jqueryui.com/effect/ + this._delay( function() { + if ( counter === this.counter ) { + //Precompute after each DOM insertion, NOT on mousemove + this.refreshPositions( !hardRefresh ); + } + } ); -var dataSpace = "ui-effects-", - dataSpaceStyle = "ui-effects-style", - dataSpaceAnimated = "ui-effects-animated"; + }, -$.effects = { - effect: {} -}; + _clear: function( event, noPropagation ) { -/******************************************************************************/ -/****************************** CLASS ANIMATIONS ******************************/ -/******************************************************************************/ -( function() { + this.reverting = false; -var classAnimationActions = [ "add", "remove", "toggle" ], - shorthandStyles = { - border: 1, - borderBottom: 1, - borderColor: 1, - borderLeft: 1, - borderRight: 1, - borderTop: 1, - borderWidth: 1, - margin: 1, - padding: 1 - }; + // We delay all events that have to be triggered to after the point where the placeholder + // has been removed and everything else normalized again + var i, + delayedTriggers = []; -$.each( - [ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], - function( _, prop ) { - $.fx.step[ prop ] = function( fx ) { - if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) { - jQuery.style( fx.elem, prop, fx.end ); - fx.setAttr = true; + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets + // reappended (see #4088) + if ( !this._noFinalSort && this.currentItem.parent().length ) { + this.placeholder.before( this.currentItem ); + } + this._noFinalSort = null; + + if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) { + for ( i in this._storedCSS ) { + if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) { + this._storedCSS[ i ] = ""; + } } - }; - } -); + this.currentItem.css( this._storedCSS ); + this._removeClass( this.currentItem, "ui-sortable-helper" ); + } else { + this.currentItem.show(); + } -function camelCase( string ) { - return string.replace( /-([\da-z])/gi, function( all, letter ) { - return letter.toUpperCase(); - } ); -} + if ( this.fromOutside && !noPropagation ) { + delayedTriggers.push( function( event ) { + this._trigger( "receive", event, this._uiHash( this.fromOutside ) ); + } ); + } + if ( ( this.fromOutside || + this.domPosition.prev !== + this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] || + this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) { -function getElementStyles( elem ) { - var key, len, - style = elem.ownerDocument.defaultView ? - elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : - elem.currentStyle, - styles = {}; + // Trigger update callback if the DOM position has changed + delayedTriggers.push( function( event ) { + this._trigger( "update", event, this._uiHash() ); + } ); + } - if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { - len = style.length; - while ( len-- ) { - key = style[ len ]; - if ( typeof style[ key ] === "string" ) { - styles[ camelCase( key ) ] = style[ key ]; + // Check if the items Container has Changed and trigger appropriate + // events. + if ( this !== this.currentContainer ) { + if ( !noPropagation ) { + delayedTriggers.push( function( event ) { + this._trigger( "remove", event, this._uiHash() ); + } ); + delayedTriggers.push( ( function( c ) { + return function( event ) { + c._trigger( "receive", event, this._uiHash( this ) ); + }; + } ).call( this, this.currentContainer ) ); + delayedTriggers.push( ( function( c ) { + return function( event ) { + c._trigger( "update", event, this._uiHash( this ) ); + }; + } ).call( this, this.currentContainer ) ); + } + } + + //Post events to containers + function delayEvent( type, instance, container ) { + return function( event ) { + container._trigger( type, event, instance._uiHash( instance ) ); + }; + } + for ( i = this.containers.length - 1; i >= 0; i-- ) { + if ( !noPropagation ) { + delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); + } + if ( this.containers[ i ].containerCache.over ) { + delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); + this.containers[ i ].containerCache.over = 0; } } - // Support: Opera, IE <9 - } else { - for ( key in style ) { - if ( typeof style[ key ] === "string" ) { - styles[ key ] = style[ key ]; - } + //Do what was originally in plugins + if ( this.storedCursor ) { + this.document.find( "body" ).css( "cursor", this.storedCursor ); + this.storedStylesheet.remove(); + } + if ( this._storedOpacity ) { + this.helper.css( "opacity", this._storedOpacity ); + } + if ( this._storedZIndex ) { + this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex ); } - } - return styles; -} + this.dragging = false; -function styleDifference( oldStyle, newStyle ) { - var diff = {}, - name, value; + if ( !noPropagation ) { + this._trigger( "beforeStop", event, this._uiHash() ); + } - for ( name in newStyle ) { - value = newStyle[ name ]; - if ( oldStyle[ name ] !== value ) { - if ( !shorthandStyles[ name ] ) { - if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) { - diff[ name ] = value; - } + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, + // it unbinds ALL events from the original node! + this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); + + if ( !this.cancelHelperRemoval ) { + if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { + this.helper.remove(); } + this.helper = null; } - } - return diff; -} + if ( !noPropagation ) { + for ( i = 0; i < delayedTriggers.length; i++ ) { -// Support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} + // Trigger all delayed events + delayedTriggers[ i ].call( this, event ); + } + this._trigger( "stop", event, this._uiHash() ); + } -$.effects.animateClass = function( value, duration, easing, callback ) { - var o = $.speed( duration, easing, callback ); + this.fromOutside = false; + return !this.cancelHelperRemoval; - return this.queue( function() { - var animated = $( this ), - baseClass = animated.attr( "class" ) || "", - applyClassChange, - allAnimations = o.children ? animated.find( "*" ).addBack() : animated; + }, - // Map the animated objects to store the original styles. - allAnimations = allAnimations.map( function() { - var el = $( this ); - return { - el: el, - start: getElementStyles( this ) - }; - } ); + _trigger: function() { + if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) { + this.cancel(); + } + }, - // Apply class change - applyClassChange = function() { - $.each( classAnimationActions, function( i, action ) { - if ( value[ action ] ) { - animated[ action + "Class" ]( value[ action ] ); - } - } ); + _uiHash: function( _inst ) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $( [] ), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null }; - applyClassChange(); - - // Map all animated objects again - calculate new styles and diff - allAnimations = allAnimations.map( function() { - this.end = getElementStyles( this.el[ 0 ] ); - this.diff = styleDifference( this.start, this.end ); - return this; - } ); - - // Apply original class - animated.attr( "class", baseClass ); - - // Map all animated objects again - this time collecting a promise - allAnimations = allAnimations.map( function() { - var styleInfo = this, - dfd = $.Deferred(), - opts = $.extend( {}, o, { - queue: false, - complete: function() { - dfd.resolve( styleInfo ); - } - } ); - - this.el.animate( this.diff, opts ); - return dfd.promise(); - } ); + } - // Once all animations have completed: - $.when.apply( $, allAnimations.get() ).done( function() { +} ); - // Set the final class - applyClassChange(); - // For each animated element, - // clear all css properties that were animated - $.each( arguments, function() { - var el = this.el; - $.each( this.diff, function( key ) { - el.css( key, "" ); - } ); - } ); +/*! + * jQuery UI Spinner 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - // This is guarnteed to be there if you use jQuery.speed() - // it also handles dequeuing the next anim... - o.complete.call( animated[ 0 ] ); - } ); - } ); -}; +//>>label: Spinner +//>>group: Widgets +//>>description: Displays buttons to easily input numbers via the keyboard or mouse. +//>>docs: http://api.jqueryui.com/spinner/ +//>>demos: http://jqueryui.com/spinner/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/spinner.css +//>>css.theme: ../../themes/base/theme.css -$.fn.extend( { - addClass: ( function( orig ) { - return function( classNames, speed, easing, callback ) { - return speed ? - $.effects.animateClass.call( this, - { add: classNames }, speed, easing, callback ) : - orig.apply( this, arguments ); - }; - } )( $.fn.addClass ), - removeClass: ( function( orig ) { - return function( classNames, speed, easing, callback ) { - return arguments.length > 1 ? - $.effects.animateClass.call( this, - { remove: classNames }, speed, easing, callback ) : - orig.apply( this, arguments ); - }; - } )( $.fn.removeClass ), +function spinnerModifier( fn ) { + return function() { + var previous = this.element.val(); + fn.apply( this, arguments ); + this._refresh(); + if ( previous !== this.element.val() ) { + this._trigger( "change" ); + } + }; +} - toggleClass: ( function( orig ) { - return function( classNames, force, speed, easing, callback ) { - if ( typeof force === "boolean" || force === undefined ) { - if ( !speed ) { +$.widget( "ui.spinner", { + version: "1.13.2", + defaultElement: "<input>", + widgetEventPrefix: "spin", + options: { + classes: { + "ui-spinner": "ui-corner-all", + "ui-spinner-down": "ui-corner-br", + "ui-spinner-up": "ui-corner-tr" + }, + culture: null, + icons: { + down: "ui-icon-triangle-1-s", + up: "ui-icon-triangle-1-n" + }, + incremental: true, + max: null, + min: null, + numberFormat: null, + page: 10, + step: 1, - // Without speed parameter - return orig.apply( this, arguments ); - } else { - return $.effects.animateClass.call( this, - ( force ? { add: classNames } : { remove: classNames } ), - speed, easing, callback ); - } - } else { + change: null, + spin: null, + start: null, + stop: null + }, - // Without force parameter - return $.effects.animateClass.call( this, - { toggle: classNames }, force, speed, easing ); - } - }; - } )( $.fn.toggleClass ), + _create: function() { - switchClass: function( remove, add, speed, easing, callback ) { - return $.effects.animateClass.call( this, { - add: add, - remove: remove - }, speed, easing, callback ); - } -} ); + // handle string values that need to be parsed + this._setOption( "max", this.options.max ); + this._setOption( "min", this.options.min ); + this._setOption( "step", this.options.step ); -} )(); + // Only format if there is a value, prevents the field from being marked + // as invalid in Firefox, see #9573. + if ( this.value() !== "" ) { -/******************************************************************************/ -/*********************************** EFFECTS **********************************/ -/******************************************************************************/ + // Format the value, but don't constrain. + this._value( this.element.val(), true ); + } -( function() { + this._draw(); + this._on( this._events ); + this._refresh(); -if ( $.expr && $.expr.pseudos && $.expr.pseudos.animated ) { - $.expr.pseudos.animated = ( function( orig ) { - return function( elem ) { - return !!$( elem ).data( dataSpaceAnimated ) || orig( elem ); - }; - } )( $.expr.pseudos.animated ); -} + // Turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } + } ); + }, -if ( $.uiBackCompat !== false ) { - $.extend( $.effects, { + _getCreateOptions: function() { + var options = this._super(); + var element = this.element; - // Saves a set of properties in a data storage - save: function( element, set ) { - var i = 0, length = set.length; - for ( ; i < length; i++ ) { - if ( set[ i ] !== null ) { - element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] ); - } + $.each( [ "min", "max", "step" ], function( i, option ) { + var value = element.attr( option ); + if ( value != null && value.length ) { + options[ option ] = value; } - }, + } ); - // Restores a set of previously saved properties from a data storage - restore: function( element, set ) { - var val, i = 0, length = set.length; - for ( ; i < length; i++ ) { - if ( set[ i ] !== null ) { - val = element.data( dataSpace + set[ i ] ); - element.css( set[ i ], val ); - } + return options; + }, + + _events: { + keydown: function( event ) { + if ( this._start( event ) && this._keydown( event ) ) { + event.preventDefault(); } }, + keyup: "_stop", + focus: function() { + this.previous = this.element.val(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } - setMode: function( el, mode ) { - if ( mode === "toggle" ) { - mode = el.is( ":hidden" ) ? "show" : "hide"; + this._stop(); + this._refresh(); + if ( this.previous !== this.element.val() ) { + this._trigger( "change", event ); } - return mode; }, + mousewheel: function( event, delta ) { + var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ); + var isActive = this.element[ 0 ] === activeElement; - // Wraps the element around a wrapper that copies position properties - createWrapper: function( element ) { + if ( !isActive || !delta ) { + return; + } - // If the element is already wrapped, return it - if ( element.parent().is( ".ui-effects-wrapper" ) ) { - return element.parent(); + if ( !this.spinning && !this._start( event ) ) { + return false; } - // Wrap the element - var props = { - width: element.outerWidth( true ), - height: element.outerHeight( true ), - "float": element.css( "float" ) - }, - wrapper = $( "<div></div>" ) - .addClass( "ui-effects-wrapper" ) - .css( { - fontSize: "100%", - background: "transparent", - border: "none", - margin: 0, - padding: 0 - } ), + this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event ); + clearTimeout( this.mousewheelTimer ); + this.mousewheelTimer = this._delay( function() { + if ( this.spinning ) { + this._stop( event ); + } + }, 100 ); + event.preventDefault(); + }, + "mousedown .ui-spinner-button": function( event ) { + var previous; - // Store the size in case width/height are defined in % - Fixes #5245 - size = { - width: element.width(), - height: element.height() - }, - active = document.activeElement; + // We never want the buttons to have focus; whenever the user is + // interacting with the spinner, the focus should be on the input. + // If the input is focused then this.previous is properly set from + // when the input first received focus. If the input is not focused + // then we need to set this.previous based on the value before spinning. + previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ? + this.previous : this.element.val(); + function checkFocus() { + var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ); + if ( !isActive ) { + this.element.trigger( "focus" ); + this.previous = previous; - // Support: Firefox - // Firefox incorrectly exposes anonymous content - // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 - try { - // eslint-disable-next-line no-unused-expressions - active.id; - } catch ( e ) { - active = document.body; + // support: IE + // IE sets focus asynchronously, so we need to check if focus + // moved off of the input because the user clicked on the button. + this._delay( function() { + this.previous = previous; + } ); + } } - element.wrap( wrapper ); + // Ensure focus is on (or stays on) the text field + event.preventDefault(); + checkFocus.call( this ); - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).trigger( "focus" ); + // Support: IE + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + // and check (again) if focus moved off of the input. + this.cancelBlur = true; + this._delay( function() { + delete this.cancelBlur; + checkFocus.call( this ); + } ); + + if ( this._start( event ) === false ) { + return; } - // Hotfix for jQuery 1.4 since some change in wrap() seems to actually - // lose the reference to the wrapped element - wrapper = element.parent(); + this._repeat( null, $( event.currentTarget ) + .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + "mouseup .ui-spinner-button": "_stop", + "mouseenter .ui-spinner-button": function( event ) { - // Transfer positioning properties to the wrapper - if ( element.css( "position" ) === "static" ) { - wrapper.css( { position: "relative" } ); - element.css( { position: "relative" } ); - } else { - $.extend( props, { - position: element.css( "position" ), - zIndex: element.css( "z-index" ) - } ); - $.each( [ "top", "left", "bottom", "right" ], function( i, pos ) { - props[ pos ] = element.css( pos ); - if ( isNaN( parseInt( props[ pos ], 10 ) ) ) { - props[ pos ] = "auto"; - } - } ); - element.css( { - position: "relative", - top: 0, - left: 0, - right: "auto", - bottom: "auto" - } ); + // button will add ui-state-active if mouse was down while mouseleave and kept down + if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { + return; } - element.css( size ); - return wrapper.css( props ).show(); + if ( this._start( event ) === false ) { + return false; + } + this._repeat( null, $( event.currentTarget ) + .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); }, - removeWrapper: function( element ) { - var active = document.activeElement; + // TODO: do we really want to consider this a stop? + // shouldn't we just stop the repeater and wait until mouseup before + // we trigger the stop event? + "mouseleave .ui-spinner-button": "_stop" + }, - if ( element.parent().is( ".ui-effects-wrapper" ) ) { - element.parent().replaceWith( element ); + // Support mobile enhanced option and make backcompat more sane + _enhance: function() { + this.uiSpinner = this.element + .attr( "autocomplete", "off" ) + .wrap( "<span>" ) + .parent() - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).trigger( "focus" ); + // Add buttons + .append( + "<a></a><a></a>" + ); + }, + + _draw: function() { + this._enhance(); + + this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" ); + this._addClass( "ui-spinner-input" ); + + this.element.attr( "role", "spinbutton" ); + + // Button bindings + this.buttons = this.uiSpinner.children( "a" ) + .attr( "tabIndex", -1 ) + .attr( "aria-hidden", true ) + .button( { + classes: { + "ui-button": "" } - } + } ); + + // TODO: Right now button does not support classes this is already updated in button PR + this._removeClass( this.buttons, "ui-corner-all" ); + + this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" ); + this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" ); + this.buttons.first().button( { + "icon": this.options.icons.up, + "showLabel": false + } ); + this.buttons.last().button( { + "icon": this.options.icons.down, + "showLabel": false + } ); - return element; + // IE 6 doesn't understand height: 50% for the buttons + // unless the wrapper has an explicit height + if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) && + this.uiSpinner.height() > 0 ) { + this.uiSpinner.height( this.uiSpinner.height() ); } - } ); -} + }, -$.extend( $.effects, { - version: "1.13.1", + _keydown: function( event ) { + var options = this.options, + keyCode = $.ui.keyCode; - define: function( name, mode, effect ) { - if ( !effect ) { - effect = mode; - mode = "effect"; + switch ( event.keyCode ) { + case keyCode.UP: + this._repeat( null, 1, event ); + return true; + case keyCode.DOWN: + this._repeat( null, -1, event ); + return true; + case keyCode.PAGE_UP: + this._repeat( null, options.page, event ); + return true; + case keyCode.PAGE_DOWN: + this._repeat( null, -options.page, event ); + return true; } - $.effects.effect[ name ] = effect; - $.effects.effect[ name ].mode = mode; - - return effect; + return false; }, - scaledDimensions: function( element, percent, direction ) { - if ( percent === 0 ) { - return { - height: 0, - width: 0, - outerHeight: 0, - outerWidth: 0 - }; + _start: function( event ) { + if ( !this.spinning && this._trigger( "start", event ) === false ) { + return false; } - var x = direction !== "horizontal" ? ( ( percent || 100 ) / 100 ) : 1, - y = direction !== "vertical" ? ( ( percent || 100 ) / 100 ) : 1; + if ( !this.counter ) { + this.counter = 1; + } + this.spinning = true; + return true; + }, - return { - height: element.height() * y, - width: element.width() * x, - outerHeight: element.outerHeight() * y, - outerWidth: element.outerWidth() * x - }; + _repeat: function( i, steps, event ) { + i = i || 500; - }, + clearTimeout( this.timer ); + this.timer = this._delay( function() { + this._repeat( 40, steps, event ); + }, i ); - clipToBox: function( animation ) { - return { - width: animation.clip.right - animation.clip.left, - height: animation.clip.bottom - animation.clip.top, - left: animation.clip.left, - top: animation.clip.top - }; + this._spin( steps * this.options.step, event ); }, - // Injects recently queued functions to be first in line (after "inprogress") - unshift: function( element, queueLength, count ) { - var queue = element.queue(); + _spin: function( step, event ) { + var value = this.value() || 0; - if ( queueLength > 1 ) { - queue.splice.apply( queue, - [ 1, 0 ].concat( queue.splice( queueLength, count ) ) ); + if ( !this.counter ) { + this.counter = 1; } - element.dequeue(); - }, - saveStyle: function( element ) { - element.data( dataSpaceStyle, element[ 0 ].style.cssText ); - }, + value = this._adjustValue( value + step * this._increment( this.counter ) ); - restoreStyle: function( element ) { - element[ 0 ].style.cssText = element.data( dataSpaceStyle ) || ""; - element.removeData( dataSpaceStyle ); + if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) { + this._value( value ); + this.counter++; + } }, - mode: function( element, mode ) { - var hidden = element.is( ":hidden" ); + _increment: function( i ) { + var incremental = this.options.incremental; - if ( mode === "toggle" ) { - mode = hidden ? "show" : "hide"; + if ( incremental ) { + return typeof incremental === "function" ? + incremental( i ) : + Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 ); } - if ( hidden ? mode === "hide" : mode === "show" ) { - mode = "none"; + + return 1; + }, + + _precision: function() { + var precision = this._precisionOf( this.options.step ); + if ( this.options.min !== null ) { + precision = Math.max( precision, this._precisionOf( this.options.min ) ); } - return mode; + return precision; }, - // Translates a [top,left] array into a baseline value - getBaseline: function( origin, original ) { - var y, x; + _precisionOf: function( num ) { + var str = num.toString(), + decimal = str.indexOf( "." ); + return decimal === -1 ? 0 : str.length - decimal - 1; + }, - switch ( origin[ 0 ] ) { - case "top": - y = 0; - break; - case "middle": - y = 0.5; - break; - case "bottom": - y = 1; - break; - default: - y = origin[ 0 ] / original.height; - } + _adjustValue: function( value ) { + var base, aboveMin, + options = this.options; - switch ( origin[ 1 ] ) { - case "left": - x = 0; - break; - case "center": - x = 0.5; - break; - case "right": - x = 1; - break; - default: - x = origin[ 1 ] / original.width; - } + // Make sure we're at a valid step + // - find out where we are relative to the base (min or 0) + base = options.min !== null ? options.min : 0; + aboveMin = value - base; - return { - x: x, - y: y - }; - }, + // - round to the nearest step + aboveMin = Math.round( aboveMin / options.step ) * options.step; - // Creates a placeholder element so that the original element can be made absolute - createPlaceholder: function( element ) { - var placeholder, - cssPosition = element.css( "position" ), - position = element.position(); + // - rounding is based on 0, so adjust back to our base + value = base + aboveMin; - // Lock in margins first to account for form elements, which - // will change margin if you explicitly set height - // see: http://jsfiddle.net/JZSMt/3/ https://bugs.webkit.org/show_bug.cgi?id=107380 - // Support: Safari - element.css( { - marginTop: element.css( "marginTop" ), - marginBottom: element.css( "marginBottom" ), - marginLeft: element.css( "marginLeft" ), - marginRight: element.css( "marginRight" ) - } ) - .outerWidth( element.outerWidth() ) - .outerHeight( element.outerHeight() ); + // Fix precision from bad JS floating point math + value = parseFloat( value.toFixed( this._precision() ) ); - if ( /^(static|relative)/.test( cssPosition ) ) { - cssPosition = "absolute"; + // Clamp the value + if ( options.max !== null && value > options.max ) { + return options.max; + } + if ( options.min !== null && value < options.min ) { + return options.min; + } - placeholder = $( "<" + element[ 0 ].nodeName + ">" ).insertAfter( element ).css( { + return value; + }, - // Convert inline to inline block to account for inline elements - // that turn to inline block based on content (like img) - display: /^(inline|ruby)/.test( element.css( "display" ) ) ? - "inline-block" : - "block", - visibility: "hidden", + _stop: function( event ) { + if ( !this.spinning ) { + return; + } - // Margins need to be set to account for margin collapse - marginTop: element.css( "marginTop" ), - marginBottom: element.css( "marginBottom" ), - marginLeft: element.css( "marginLeft" ), - marginRight: element.css( "marginRight" ), - "float": element.css( "float" ) - } ) - .outerWidth( element.outerWidth() ) - .outerHeight( element.outerHeight() ) - .addClass( "ui-effects-placeholder" ); + clearTimeout( this.timer ); + clearTimeout( this.mousewheelTimer ); + this.counter = 0; + this.spinning = false; + this._trigger( "stop", event ); + }, - element.data( dataSpace + "placeholder", placeholder ); + _setOption: function( key, value ) { + var prevValue, first, last; + + if ( key === "culture" || key === "numberFormat" ) { + prevValue = this._parse( this.element.val() ); + this.options[ key ] = value; + this.element.val( this._format( prevValue ) ); + return; } - element.css( { - position: cssPosition, - left: position.left, - top: position.top - } ); + if ( key === "max" || key === "min" || key === "step" ) { + if ( typeof value === "string" ) { + value = this._parse( value ); + } + } + if ( key === "icons" ) { + first = this.buttons.first().find( ".ui-icon" ); + this._removeClass( first, null, this.options.icons.up ); + this._addClass( first, null, value.up ); + last = this.buttons.last().find( ".ui-icon" ); + this._removeClass( last, null, this.options.icons.down ); + this._addClass( last, null, value.down ); + } + + this._super( key, value ); + }, - return placeholder; + _setOptionDisabled: function( value ) { + this._super( value ); + + this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value ); + this.element.prop( "disabled", !!value ); + this.buttons.button( value ? "disable" : "enable" ); }, - removePlaceholder: function( element ) { - var dataKey = dataSpace + "placeholder", - placeholder = element.data( dataKey ); + _setOptions: spinnerModifier( function( options ) { + this._super( options ); + } ), - if ( placeholder ) { - placeholder.remove(); - element.removeData( dataKey ); + _parse: function( val ) { + if ( typeof val === "string" && val !== "" ) { + val = window.Globalize && this.options.numberFormat ? + Globalize.parseFloat( val, 10, this.options.culture ) : +val; } + return val === "" || isNaN( val ) ? null : val; }, - // Removes a placeholder if it exists and restores - // properties that were modified during placeholder creation - cleanUp: function( element ) { - $.effects.restoreStyle( element ); - $.effects.removePlaceholder( element ); + _format: function( value ) { + if ( value === "" ) { + return ""; + } + return window.Globalize && this.options.numberFormat ? + Globalize.format( value, this.options.numberFormat, this.options.culture ) : + value; }, - setTransition: function( element, list, factor, value ) { - value = value || {}; - $.each( list, function( i, x ) { - var unit = element.cssUnit( x ); - if ( unit[ 0 ] > 0 ) { - value[ x ] = unit[ 0 ] * factor + unit[ 1 ]; - } - } ); - return value; - } -} ); + _refresh: function() { + this.element.attr( { + "aria-valuemin": this.options.min, + "aria-valuemax": this.options.max, -// Return an effect options object for the given parameters: -function _normalizeArguments( effect, options, speed, callback ) { + // TODO: what should we do with values that can't be parsed? + "aria-valuenow": this._parse( this.element.val() ) + } ); + }, - // Allow passing all options as the first parameter - if ( $.isPlainObject( effect ) ) { - options = effect; - effect = effect.effect; - } + isValid: function() { + var value = this.value(); - // Convert to an object - effect = { effect: effect }; + // Null is invalid + if ( value === null ) { + return false; + } - // Catch (effect, null, ...) - if ( options == null ) { - options = {}; - } + // If value gets adjusted, it's invalid + return value === this._adjustValue( value ); + }, - // Catch (effect, callback) - if ( typeof options === "function" ) { - callback = options; - speed = null; - options = {}; - } + // Update the value without triggering change + _value: function( value, allowAny ) { + var parsed; + if ( value !== "" ) { + parsed = this._parse( value ); + if ( parsed !== null ) { + if ( !allowAny ) { + parsed = this._adjustValue( parsed ); + } + value = this._format( parsed ); + } + } + this.element.val( value ); + this._refresh(); + }, - // Catch (effect, speed, ?) - if ( typeof options === "number" || $.fx.speeds[ options ] ) { - callback = speed; - speed = options; - options = {}; - } + _destroy: function() { + this.element + .prop( "disabled", false ) + .removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" ); - // Catch (effect, options, callback) - if ( typeof speed === "function" ) { - callback = speed; - speed = null; - } + this.uiSpinner.replaceWith( this.element ); + }, - // Add options to effect - if ( options ) { - $.extend( effect, options ); - } + stepUp: spinnerModifier( function( steps ) { + this._stepUp( steps ); + } ), + _stepUp: function( steps ) { + if ( this._start() ) { + this._spin( ( steps || 1 ) * this.options.step ); + this._stop(); + } + }, - speed = speed || options.duration; - effect.duration = $.fx.off ? 0 : - typeof speed === "number" ? speed : - speed in $.fx.speeds ? $.fx.speeds[ speed ] : - $.fx.speeds._default; + stepDown: spinnerModifier( function( steps ) { + this._stepDown( steps ); + } ), + _stepDown: function( steps ) { + if ( this._start() ) { + this._spin( ( steps || 1 ) * -this.options.step ); + this._stop(); + } + }, - effect.complete = callback || options.complete; + pageUp: spinnerModifier( function( pages ) { + this._stepUp( ( pages || 1 ) * this.options.page ); + } ), - return effect; -} + pageDown: spinnerModifier( function( pages ) { + this._stepDown( ( pages || 1 ) * this.options.page ); + } ), -function standardAnimationOption( option ) { + value: function( newVal ) { + if ( !arguments.length ) { + return this._parse( this.element.val() ); + } + spinnerModifier( this._value ).call( this, newVal ); + }, - // Valid standard speeds (nothing, number, named speed) - if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) { - return true; + widget: function() { + return this.uiSpinner; } +} ); - // Invalid strings - treat as "normal" speed - if ( typeof option === "string" && !$.effects.effect[ option ] ) { - return true; - } +// DEPRECATED +// TODO: switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { - // Complete callback - if ( typeof option === "function" ) { - return true; - } + // Backcompat for spinner html extension points + $.widget( "ui.spinner", $.ui.spinner, { + _enhance: function() { + this.uiSpinner = this.element + .attr( "autocomplete", "off" ) + .wrap( this._uiSpinnerHtml() ) + .parent() - // Options hash (but not naming an effect) - if ( typeof option === "object" && !option.effect ) { - return true; - } + // Add buttons + .append( this._buttonHtml() ); + }, + _uiSpinnerHtml: function() { + return "<span>"; + }, - // Didn't match any standard API - return false; + _buttonHtml: function() { + return "<a></a><a></a>"; + } + } ); } -$.fn.extend( { - effect: function( /* effect, options, speed, callback */ ) { - var args = _normalizeArguments.apply( this, arguments ), - effectMethod = $.effects.effect[ args.effect ], - defaultMode = effectMethod.mode, - queue = args.queue, - queueName = queue || "fx", - complete = args.complete, - mode = args.mode, - modes = [], - prefilter = function( next ) { - var el = $( this ), - normalizedMode = $.effects.mode( el, mode ) || defaultMode; - - // Sentinel for duck-punching the :animated pseudo-selector - el.data( dataSpaceAnimated, true ); +var widgetsSpinner = $.ui.spinner; - // Save effect mode for later use, - // we can't just call $.effects.mode again later, - // as the .show() below destroys the initial state - modes.push( normalizedMode ); - // See $.uiBackCompat inside of run() for removal of defaultMode in 1.14 - if ( defaultMode && ( normalizedMode === "show" || - ( normalizedMode === defaultMode && normalizedMode === "hide" ) ) ) { - el.show(); - } +/*! + * jQuery UI Tabs 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ - if ( !defaultMode || normalizedMode !== "none" ) { - $.effects.saveStyle( el ); - } +//>>label: Tabs +//>>group: Widgets +//>>description: Transforms a set of container elements into a tab structure. +//>>docs: http://api.jqueryui.com/tabs/ +//>>demos: http://jqueryui.com/tabs/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/tabs.css +//>>css.theme: ../../themes/base/theme.css - if ( typeof next === "function" ) { - next(); - } - }; - if ( $.fx.off || !effectMethod ) { +$.widget( "ui.tabs", { + version: "1.13.2", + delay: 300, + options: { + active: null, + classes: { + "ui-tabs": "ui-corner-all", + "ui-tabs-nav": "ui-corner-all", + "ui-tabs-panel": "ui-corner-bottom", + "ui-tabs-tab": "ui-corner-top" + }, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, - // Delegate to the original method (e.g., .show()) if possible - if ( mode ) { - return this[ mode ]( args.duration, complete ); - } else { - return this.each( function() { - if ( complete ) { - complete.call( this ); - } - } ); - } - } + // Callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null + }, - function run( next ) { - var elem = $( this ); + _isLocal: ( function() { + var rhash = /#.*$/; - function cleanup() { - elem.removeData( dataSpaceAnimated ); + return function( anchor ) { + var anchorUrl, locationUrl; - $.effects.cleanUp( elem ); + anchorUrl = anchor.href.replace( rhash, "" ); + locationUrl = location.href.replace( rhash, "" ); - if ( args.mode === "hide" ) { - elem.hide(); - } + // Decoding may throw an error if the URL isn't UTF-8 (#9518) + try { + anchorUrl = decodeURIComponent( anchorUrl ); + } catch ( error ) {} + try { + locationUrl = decodeURIComponent( locationUrl ); + } catch ( error ) {} - done(); - } + return anchor.hash.length > 1 && anchorUrl === locationUrl; + }; + } )(), - function done() { - if ( typeof complete === "function" ) { - complete.call( elem[ 0 ] ); - } + _create: function() { + var that = this, + options = this.options; - if ( typeof next === "function" ) { - next(); - } - } + this.running = false; - // Override mode option on a per element basis, - // as toggle can be either show or hide depending on element state - args.mode = modes.shift(); + this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); + this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); - if ( $.uiBackCompat !== false && !defaultMode ) { - if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { + this._processTabs(); + options.active = this._initialActive(); - // Call the core method to track "olddisplay" properly - elem[ mode ](); - done(); - } else { - effectMethod.call( elem[ 0 ], args, done ); - } - } else { - if ( args.mode === "none" ) { + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( Array.isArray( options.disabled ) ) { + options.disabled = $.uniqueSort( options.disabled.concat( + $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { + return that.tabs.index( li ); + } ) + ) ).sort(); + } - // Call the core method to track "olddisplay" properly - elem[ mode ](); - done(); - } else { - effectMethod.call( elem[ 0 ], args, cleanup ); - } - } + // Check for length avoids error when initializing empty list + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( options.active ); + } else { + this.active = $(); } - // Run prefilter on all elements first to ensure that - // any showing or hiding happens before placeholder creation, - // which ensures that any layout changes are correctly captured. - return queue === false ? - this.each( prefilter ).each( run ) : - this.queue( queueName, prefilter ).queue( queueName, run ); + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } }, - show: ( function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "show"; - return this.effect.call( this, args ); - } - }; - } )( $.fn.show ), + _initialActive: function() { + var active = this.options.active, + collapsible = this.options.collapsible, + locationHash = location.hash.substring( 1 ); - hide: ( function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "hide"; - return this.effect.call( this, args ); + if ( active === null ) { + + // check the fragment identifier in the URL + if ( locationHash ) { + this.tabs.each( function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { + active = i; + return false; + } + } ); } - }; - } )( $.fn.hide ), - toggle: ( function( orig ) { - return function( option ) { - if ( standardAnimationOption( option ) || typeof option === "boolean" ) { - return orig.apply( this, arguments ); - } else { - var args = _normalizeArguments.apply( this, arguments ); - args.mode = "toggle"; - return this.effect.call( this, args ); + // Check for a tab marked active via a class + if ( active === null ) { + active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); } - }; - } )( $.fn.toggle ), - cssUnit: function( key ) { - var style = this.css( key ), - val = []; + // No active tab, set to false + if ( active === null || active === -1 ) { + active = this.tabs.length ? 0 : false; + } + } - $.each( [ "em", "px", "%", "pt" ], function( i, unit ) { - if ( style.indexOf( unit ) > 0 ) { - val = [ parseFloat( style ), unit ]; + // Handle numbers: negative, out of range + if ( active !== false ) { + active = this.tabs.index( this.tabs.eq( active ) ); + if ( active === -1 ) { + active = collapsible ? false : 0; } - } ); - return val; - }, + } - cssClip: function( clipObj ) { - if ( clipObj ) { - return this.css( "clip", "rect(" + clipObj.top + "px " + clipObj.right + "px " + - clipObj.bottom + "px " + clipObj.left + "px)" ); + // Don't allow collapsible: false and active: false + if ( !collapsible && active === false && this.anchors.length ) { + active = 0; } - return parseClip( this.css( "clip" ), this ); + + return active; }, - transfer: function( options, done ) { - var element = $( this ), - target = $( options.to ), - targetFixed = target.css( "position" ) === "fixed", - body = $( "body" ), - fixTop = targetFixed ? body.scrollTop() : 0, - fixLeft = targetFixed ? body.scrollLeft() : 0, - endPosition = target.offset(), - animation = { - top: endPosition.top - fixTop, - left: endPosition.left - fixLeft, - height: target.innerHeight(), - width: target.innerWidth() - }, - startPosition = element.offset(), - transfer = $( "<div class='ui-effects-transfer'></div>" ); + _getCreateEventData: function() { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + }; + }, - transfer - .appendTo( "body" ) - .addClass( options.className ) - .css( { - top: startPosition.top - fixTop, - left: startPosition.left - fixLeft, - height: element.innerHeight(), - width: element.innerWidth(), - position: targetFixed ? "fixed" : "absolute" - } ) - .animate( animation, options.duration, options.easing, function() { - transfer.remove(); - if ( typeof done === "function" ) { - done(); - } - } ); - } -} ); + _tabKeydown: function( event ) { + var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), + selectedIndex = this.tabs.index( focusedTab ), + goingForward = true; -function parseClip( str, element ) { - var outerWidth = element.outerWidth(), - outerHeight = element.outerHeight(), - clipRegex = /^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/, - values = clipRegex.exec( str ) || [ "", 0, outerWidth, outerHeight, 0 ]; + if ( this._handlePageNav( event ) ) { + return; + } - return { - top: parseFloat( values[ 1 ] ) || 0, - right: values[ 2 ] === "auto" ? outerWidth : parseFloat( values[ 2 ] ), - bottom: values[ 3 ] === "auto" ? outerHeight : parseFloat( values[ 3 ] ), - left: parseFloat( values[ 4 ] ) || 0 - }; -} + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: -$.fx.step.clip = function( fx ) { - if ( !fx.clipInit ) { - fx.start = $( fx.elem ).cssClip(); - if ( typeof fx.end === "string" ) { - fx.end = parseClip( fx.end, fx.elem ); - } - fx.clipInit = true; - } + // Activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; + case $.ui.keyCode.ENTER: - $( fx.elem ).cssClip( { - top: fx.pos * ( fx.end.top - fx.start.top ) + fx.start.top, - right: fx.pos * ( fx.end.right - fx.start.right ) + fx.start.right, - bottom: fx.pos * ( fx.end.bottom - fx.start.bottom ) + fx.start.bottom, - left: fx.pos * ( fx.end.left - fx.start.left ) + fx.start.left - } ); -}; + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout( this.activating ); -} )(); + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); + return; + default: + return; + } -/******************************************************************************/ -/*********************************** EASING ***********************************/ -/******************************************************************************/ + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); -( function() { + // Navigating with control/command key will prevent automatic activation + if ( !event.ctrlKey && !event.metaKey ) { -// Based on easing equations from Robert Penner (http://www.robertpenner.com/easing) + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); -var baseEasings = {}; + this.activating = this._delay( function() { + this.option( "active", selectedIndex ); + }, this.delay ); + } + }, -$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) { - baseEasings[ name ] = function( p ) { - return Math.pow( p, i + 2 ); - }; -} ); + _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } -$.extend( baseEasings, { - Sine: function( p ) { - return 1 - Math.cos( p * Math.PI / 2 ); + // Ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { + event.preventDefault(); + this.active.trigger( "focus" ); + } }, - Circ: function( p ) { - return 1 - Math.sqrt( 1 - p * p ); + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; + } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; + } }, - Elastic: function( p ) { - return p === 0 || p === 1 ? p : - -Math.pow( 2, 8 * ( p - 1 ) ) * Math.sin( ( ( p - 1 ) * 80 - 7.5 ) * Math.PI / 15 ); + + _findNextTab: function( index, goingForward ) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; + } + + return index; }, - Back: function( p ) { - return p * p * ( 3 * p - 2 ); + + _focusNextTab: function( index, goingForward ) { + index = this._findNextTab( index, goingForward ); + this.tabs.eq( index ).trigger( "focus" ); + return index; }, - Bounce: function( p ) { - var pow2, - bounce = 4; - while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} - return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); - } -} ); + _setOption: function( key, value ) { + if ( key === "active" ) { -$.each( baseEasings, function( name, easeIn ) { - $.easing[ "easeIn" + name ] = easeIn; - $.easing[ "easeOut" + name ] = function( p ) { - return 1 - easeIn( 1 - p ); - }; - $.easing[ "easeInOut" + name ] = function( p ) { - return p < 0.5 ? - easeIn( p * 2 ) / 2 : - 1 - easeIn( p * -2 + 2 ) / 2; - }; -} ); + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } -} )(); + this._super( key, value ); -var effect = $.effects; + if ( key === "collapsible" ) { + this._toggleClass( "ui-tabs-collapsible", null, value ); + // Setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } + } -/*! - * jQuery UI Effects Blind 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + if ( key === "event" ) { + this._setupEvents( value ); + } -//>>label: Blind Effect -//>>group: Effects -//>>description: Blinds the element. -//>>docs: http://api.jqueryui.com/blind-effect/ -//>>demos: http://jqueryui.com/effect/ + if ( key === "heightStyle" ) { + this._setupHeightStyle( value ); + } + }, + _sanitizeSelector: function( hash ) { + return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; + }, -var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, done ) { - var map = { - up: [ "bottom", "top" ], - vertical: [ "bottom", "top" ], - down: [ "top", "bottom" ], - left: [ "right", "left" ], - horizontal: [ "right", "left" ], - right: [ "left", "right" ] - }, - element = $( this ), - direction = options.direction || "up", - start = element.cssClip(), - animate = { clip: $.extend( {}, start ) }, - placeholder = $.effects.createPlaceholder( element ); + refresh: function() { + var options = this.options, + lis = this.tablist.children( ":has(a[href])" ); - animate.clip[ map[ direction ][ 0 ] ] = animate.clip[ map[ direction ][ 1 ] ]; + // Get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { + return lis.index( tab ); + } ); - if ( options.mode === "show" ) { - element.cssClip( animate.clip ); - if ( placeholder ) { - placeholder.css( $.effects.clipToBox( animate ) ); - } + this._processTabs(); - animate.clip = start; - } + // Was collapsed or no tabs + if ( options.active === false || !this.anchors.length ) { + options.active = false; + this.active = $(); - if ( placeholder ) { - placeholder.animate( $.effects.clipToBox( animate ), options.duration, options.easing ); - } + // was active, but active tab is gone + } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { - element.animate( animate, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); -} ); + // all remaining tabs are disabled + if ( this.tabs.length === options.disabled.length ) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); + } -/*! - * jQuery UI Effects Bounce 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // was active, active tab still exists + } else { -//>>label: Bounce Effect -//>>group: Effects -//>>description: Bounces an element horizontally or vertically n times. -//>>docs: http://api.jqueryui.com/bounce-effect/ -//>>demos: http://jqueryui.com/effect/ + // make sure active index is correct + options.active = this.tabs.index( this.active ); + } + + this._refresh(); + }, + + _refresh: function() { + this._setOptionDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); + this._setupHeightStyle( this.options.heightStyle ); + this.tabs.not( this.active ).attr( { + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + } ); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr( { + "aria-hidden": "true" + } ); -var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) { - var upAnim, downAnim, refValue, - element = $( this ), + // Make sure one tab is in the tab order + if ( !this.active.length ) { + this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); + this._getPanelForTab( this.active ) + .show() + .attr( { + "aria-hidden": "false" + } ); + } + }, - // Defaults: - mode = options.mode, - hide = mode === "hide", - show = mode === "show", - direction = options.direction || "up", - distance = options.distance, - times = options.times || 5, + _processTabs: function() { + var that = this, + prevTabs = this.tabs, + prevAnchors = this.anchors, + prevPanels = this.panels; - // Number of internal animations - anims = times * 2 + ( show || hide ? 1 : 0 ), - speed = options.duration / anims, - easing = options.easing, + this.tablist = this._getList().attr( "role", "tablist" ); + this._addClass( this.tablist, "ui-tabs-nav", + "ui-helper-reset ui-helper-clearfix ui-widget-header" ); - // Utility: - ref = ( direction === "up" || direction === "down" ) ? "top" : "left", - motion = ( direction === "up" || direction === "left" ), - i = 0, + // Prevent users from focusing disabled tabs via click + this.tablist + .on( "mousedown" + this.eventNamespace, "> li", function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + } ) - queuelen = element.queue().length; + // Support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + } ); - $.effects.createPlaceholder( element ); + this.tabs = this.tablist.find( "> li:has(a[href])" ) + .attr( { + role: "tab", + tabIndex: -1 + } ); + this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); - refValue = element.css( ref ); + this.anchors = this.tabs.map( function() { + return $( "a", this )[ 0 ]; + } ) + .attr( { + tabIndex: -1 + } ); + this._addClass( this.anchors, "ui-tabs-anchor" ); - // Default distance for the BIGGEST bounce is the outer Distance / 3 - if ( !distance ) { - distance = element[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3; - } + this.panels = $(); - if ( show ) { - downAnim = { opacity: 1 }; - downAnim[ ref ] = refValue; + this.anchors.each( function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ), + originalAriaControls = tab.attr( "aria-controls" ); - // If we are showing, force opacity 0 and set the initial position - // then do the "first" animation - element - .css( "opacity", 0 ) - .css( ref, motion ? -distance * 2 : distance * 2 ) - .animate( downAnim, speed, easing ); - } + // Inline tab + if ( that._isLocal( anchor ) ) { + selector = anchor.hash; + panelId = selector.substring( 1 ); + panel = that.element.find( that._sanitizeSelector( selector ) ); - // Start at the smallest distance if we are hiding - if ( hide ) { - distance = distance / Math.pow( 2, times - 1 ); - } + // remote tab + } else { - downAnim = {}; - downAnim[ ref ] = refValue; + // If the tab doesn't already have aria-controls, + // generate an id by using a throw-away element + panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; + selector = "#" + panelId; + panel = that.element.find( selector ); + if ( !panel.length ) { + panel = that._createPanel( panelId ); + panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); + } + panel.attr( "aria-live", "polite" ); + } - // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here - for ( ; i < times; i++ ) { - upAnim = {}; - upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + if ( panel.length ) { + that.panels = that.panels.add( panel ); + } + if ( originalAriaControls ) { + tab.data( "ui-tabs-aria-controls", originalAriaControls ); + } + tab.attr( { + "aria-controls": panelId, + "aria-labelledby": anchorId + } ); + panel.attr( "aria-labelledby", anchorId ); + } ); - element - .animate( upAnim, speed, easing ) - .animate( downAnim, speed, easing ); + this.panels.attr( "role", "tabpanel" ); + this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); - distance = hide ? distance * 2 : distance / 2; - } + // Avoid memory leaks (#10056) + if ( prevTabs ) { + this._off( prevTabs.not( this.tabs ) ); + this._off( prevAnchors.not( this.anchors ) ); + this._off( prevPanels.not( this.panels ) ); + } + }, - // Last Bounce when Hiding - if ( hide ) { - upAnim = { opacity: 0 }; - upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + // Allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function() { + return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); + }, - element.animate( upAnim, speed, easing ); - } + _createPanel: function( id ) { + return $( "<div>" ) + .attr( "id", id ) + .data( "ui-tabs-destroy", true ); + }, - element.queue( done ); + _setOptionDisabled: function( disabled ) { + var currentItem, li, i; - $.effects.unshift( element, queuelen, anims + 1 ); -} ); + if ( Array.isArray( disabled ) ) { + if ( !disabled.length ) { + disabled = false; + } else if ( disabled.length === this.anchors.length ) { + disabled = true; + } + } + // Disable tabs + for ( i = 0; ( li = this.tabs[ i ] ); i++ ) { + currentItem = $( li ); + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + currentItem.attr( "aria-disabled", "true" ); + this._addClass( currentItem, null, "ui-state-disabled" ); + } else { + currentItem.removeAttr( "aria-disabled" ); + this._removeClass( currentItem, null, "ui-state-disabled" ); + } + } -/*! - * jQuery UI Effects Clip 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this.options.disabled = disabled; -//>>label: Clip Effect -//>>group: Effects -//>>description: Clips the element on and off like an old TV. -//>>docs: http://api.jqueryui.com/clip-effect/ -//>>demos: http://jqueryui.com/effect/ + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, + disabled === true ); + }, + _setupEvents: function( event ) { + var events = {}; + if ( event ) { + $.each( event.split( " " ), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + } ); + } -var effectsEffectClip = $.effects.define( "clip", "hide", function( options, done ) { - var start, - animate = {}, - element = $( this ), - direction = options.direction || "vertical", - both = direction === "both", - horizontal = both || direction === "horizontal", - vertical = both || direction === "vertical"; + this._off( this.anchors.add( this.tabs ).add( this.panels ) ); - start = element.cssClip(); - animate.clip = { - top: vertical ? ( start.bottom - start.top ) / 2 : start.top, - right: horizontal ? ( start.right - start.left ) / 2 : start.right, - bottom: vertical ? ( start.bottom - start.top ) / 2 : start.bottom, - left: horizontal ? ( start.right - start.left ) / 2 : start.left - }; + // Always prevent the default action, even when disabled + this._on( true, this.anchors, { + click: function( event ) { + event.preventDefault(); + } + } ); + this._on( this.anchors, events ); + this._on( this.tabs, { keydown: "_tabKeydown" } ); + this._on( this.panels, { keydown: "_panelKeydown" } ); - $.effects.createPlaceholder( element ); + this._focusable( this.tabs ); + this._hoverable( this.tabs ); + }, - if ( options.mode === "show" ) { - element.cssClip( animate.clip ); - animate.clip = start; - } + _setupHeightStyle: function( heightStyle ) { + var maxHeight, + parent = this.element.parent(); - element.animate( animate, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + maxHeight -= this.element.outerHeight() - this.element.height(); -} ); + this.element.siblings( ":visible" ).each( function() { + var elem = $( this ), + position = elem.css( "position" ); + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + } ); -/*! - * jQuery UI Effects Drop 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this.element.children().not( this.panels ).each( function() { + maxHeight -= $( this ).outerHeight( true ); + } ); -//>>label: Drop Effect -//>>group: Effects -//>>description: Moves an element in one direction and hides it at the same time. -//>>docs: http://api.jqueryui.com/drop-effect/ -//>>demos: http://jqueryui.com/effect/ + this.panels.each( function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + } ) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.panels.each( function() { + maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); + } ).height( maxHeight ); + } + }, + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + anchor = $( event.currentTarget ), + tab = anchor.closest( "li" ), + clickedIsActive = tab[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab( tab ), + toHide = !active.length ? $() : this._getPanelForTab( active ), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; -var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, done ) { + event.preventDefault(); - var distance, - element = $( this ), - mode = options.mode, - show = mode === "show", - direction = options.direction || "left", - ref = ( direction === "up" || direction === "down" ) ? "top" : "left", - motion = ( direction === "up" || direction === "left" ) ? "-=" : "+=", - oppositeMotion = ( motion === "+=" ) ? "-=" : "+=", - animation = { - opacity: 0 - }; + if ( tab.hasClass( "ui-state-disabled" ) || - $.effects.createPlaceholder( element ); + // tab is already loading + tab.hasClass( "ui-tabs-loading" ) || - distance = options.distance || - element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ) / 2; + // can't switch durning an animation + this.running || - animation[ ref ] = motion + distance; + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || - if ( show ) { - element.css( animation ); + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } - animation[ ref ] = oppositeMotion + distance; - animation.opacity = 1; - } + options.active = collapsing ? false : this.tabs.index( tab ); - // Animate - element.animate( animation, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); -} ); + this.active = clickedIsActive ? $() : tab; + if ( this.xhr ) { + this.xhr.abort(); + } + if ( !toHide.length && !toShow.length ) { + $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); + } -/*! - * jQuery UI Effects Explode 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + if ( toShow.length ) { + this.load( this.tabs.index( tab ), event ); + } + this._toggle( event, eventData ); + }, -//>>label: Explode Effect -//>>group: Effects -/* eslint-disable max-len */ -//>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness. -/* eslint-enable max-len */ -//>>docs: http://api.jqueryui.com/explode-effect/ -//>>demos: http://jqueryui.com/effect/ + // Handles show/hide for selecting tabs + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; + this.running = true; -var effectsEffectExplode = $.effects.define( "explode", "hide", function( options, done ) { + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); + } - var i, j, left, top, mx, my, - rows = options.pieces ? Math.round( Math.sqrt( options.pieces ) ) : 3, - cells = rows, - element = $( this ), - mode = options.mode, - show = mode === "show", + function show() { + that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" ); - // Show and then visibility:hidden the element before calculating offset - offset = element.show().css( "visibility", "hidden" ).offset(), + if ( toShow.length && that.options.show ) { + that._show( toShow, that.options.show, complete ); + } else { + toShow.show(); + complete(); + } + } - // Width and height of a piece - width = Math.ceil( element.outerWidth() / cells ), - height = Math.ceil( element.outerHeight() / rows ), - pieces = []; + // Start out by hiding, then showing, then completing + if ( toHide.length && this.options.hide ) { + this._hide( toHide, this.options.hide, function() { + that._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + show(); + } ); + } else { + this._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + toHide.hide(); + show(); + } - // Children animate complete: - function childComplete() { - pieces.push( this ); - if ( pieces.length === rows * cells ) { - animComplete(); + toHide.attr( "aria-hidden", "true" ); + eventData.oldTab.attr( { + "aria-selected": "false", + "aria-expanded": "false" + } ); + + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.tabs.filter( function() { + return $( this ).attr( "tabIndex" ) === 0; + } ) + .attr( "tabIndex", -1 ); } - } - // Clone the element for each row and cell. - for ( i = 0; i < rows; i++ ) { // ===> - top = offset.top + i * height; - my = i - ( rows - 1 ) / 2; + toShow.attr( "aria-hidden", "false" ); + eventData.newTab.attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + }, - for ( j = 0; j < cells; j++ ) { // ||| - left = offset.left + j * width; - mx = j - ( cells - 1 ) / 2; + _activate: function( index ) { + var anchor, + active = this._findActive( index ); - // Create a clone of the now hidden main element that will be absolute positioned - // within a wrapper div off the -left and -top equal to size of our pieces - element - .clone() - .appendTo( "body" ) - .wrap( "<div></div>" ) - .css( { - position: "absolute", - visibility: "visible", - left: -j * width, - top: -i * height - } ) + // Trying to activate the already active panel + if ( active[ 0 ] === this.active[ 0 ] ) { + return; + } - // Select the wrapper - make it overflow: hidden and absolute positioned based on - // where the original was located +left and +top equal to the size of pieces - .parent() - .addClass( "ui-effects-explode" ) - .css( { - position: "absolute", - overflow: "hidden", - width: width, - height: height, - left: left + ( show ? mx * width : 0 ), - top: top + ( show ? my * height : 0 ), - opacity: show ? 0 : 1 - } ) - .animate( { - left: left + ( show ? 0 : mx * width ), - top: top + ( show ? 0 : my * height ), - opacity: show ? 1 : 0 - }, options.duration || 500, options.easing, childComplete ); + // Trying to collapse, simulate a click on the current active header + if ( !active.length ) { + active = this.active; } - } - function animComplete() { - element.css( { - visibility: "visible" + anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; + this._eventHandler( { + target: anchor, + currentTarget: anchor, + preventDefault: $.noop } ); - $( pieces ).remove(); - done(); - } -} ); + }, + _findActive: function( index ) { + return index === false ? $() : this.tabs.eq( index ); + }, -/*! - * jQuery UI Effects Fade 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + _getIndex: function( index ) { -//>>label: Fade Effect -//>>group: Effects -//>>description: Fades the element. -//>>docs: http://api.jqueryui.com/fade-effect/ -//>>demos: http://jqueryui.com/effect/ + // meta-function to give users option to provide a href string instead of a numerical index. + if ( typeof index === "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + + $.escapeSelector( index ) + "']" ) ); + } + return index; + }, -var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, done ) { - var show = options.mode === "show"; + _destroy: function() { + if ( this.xhr ) { + this.xhr.abort(); + } - $( this ) - .css( "opacity", show ? 0 : 1 ) - .animate( { - opacity: show ? 1 : 0 - }, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); -} ); + this.tablist + .removeAttr( "role" ) + .off( this.eventNamespace ); + this.anchors + .removeAttr( "role tabIndex" ) + .removeUniqueId(); -/*! - * jQuery UI Effects Fold 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + this.tabs.add( this.panels ).each( function() { + if ( $.data( this, "ui-tabs-destroy" ) ) { + $( this ).remove(); + } else { + $( this ).removeAttr( "role tabIndex " + + "aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" ); + } + } ); -//>>label: Fold Effect -//>>group: Effects -//>>description: Folds an element first horizontally and then vertically. -//>>docs: http://api.jqueryui.com/fold-effect/ -//>>demos: http://jqueryui.com/effect/ + this.tabs.each( function() { + var li = $( this ), + prev = li.data( "ui-tabs-aria-controls" ); + if ( prev ) { + li + .attr( "aria-controls", prev ) + .removeData( "ui-tabs-aria-controls" ); + } else { + li.removeAttr( "aria-controls" ); + } + } ); + this.panels.show(); -var effectsEffectFold = $.effects.define( "fold", "hide", function( options, done ) { + if ( this.options.heightStyle !== "content" ) { + this.panels.css( "height", "" ); + } + }, - // Create element - var element = $( this ), - mode = options.mode, - show = mode === "show", - hide = mode === "hide", - size = options.size || 15, - percent = /([0-9]+)%/.exec( size ), - horizFirst = !!options.horizFirst, - ref = horizFirst ? [ "right", "bottom" ] : [ "bottom", "right" ], - duration = options.duration / 2, + enable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === false ) { + return; + } - placeholder = $.effects.createPlaceholder( element ), + if ( index === undefined ) { + disabled = false; + } else { + index = this._getIndex( index ); + if ( Array.isArray( disabled ) ) { + disabled = $.map( disabled, function( num ) { + return num !== index ? num : null; + } ); + } else { + disabled = $.map( this.tabs, function( li, num ) { + return num !== index ? num : null; + } ); + } + } + this._setOptionDisabled( disabled ); + }, - start = element.cssClip(), - animation1 = { clip: $.extend( {}, start ) }, - animation2 = { clip: $.extend( {}, start ) }, + disable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === true ) { + return; + } - distance = [ start[ ref[ 0 ] ], start[ ref[ 1 ] ] ], + if ( index === undefined ) { + disabled = true; + } else { + index = this._getIndex( index ); + if ( $.inArray( index, disabled ) !== -1 ) { + return; + } + if ( Array.isArray( disabled ) ) { + disabled = $.merge( [ index ], disabled ).sort(); + } else { + disabled = [ index ]; + } + } + this._setOptionDisabled( disabled ); + }, - queuelen = element.queue().length; + load: function( index, event ) { + index = this._getIndex( index ); + var that = this, + tab = this.tabs.eq( index ), + anchor = tab.find( ".ui-tabs-anchor" ), + panel = this._getPanelForTab( tab ), + eventData = { + tab: tab, + panel: panel + }, + complete = function( jqXHR, status ) { + if ( status === "abort" ) { + that.panels.stop( false, true ); + } - if ( percent ) { - size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ]; - } - animation1.clip[ ref[ 0 ] ] = size; - animation2.clip[ ref[ 0 ] ] = size; - animation2.clip[ ref[ 1 ] ] = 0; + that._removeClass( tab, "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); - if ( show ) { - element.cssClip( animation2.clip ); - if ( placeholder ) { - placeholder.css( $.effects.clipToBox( animation2 ) ); - } + if ( jqXHR === that.xhr ) { + delete that.xhr; + } + }; - animation2.clip = start; - } + // Not remote + if ( this._isLocal( anchor[ 0 ] ) ) { + return; + } - // Animate - element - .queue( function( next ) { - if ( placeholder ) { - placeholder - .animate( $.effects.clipToBox( animation1 ), duration, options.easing ) - .animate( $.effects.clipToBox( animation2 ), duration, options.easing ); - } + this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); - next(); - } ) - .animate( animation1, duration, options.easing ) - .animate( animation2, duration, options.easing ) - .queue( done ); + // Support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if ( this.xhr && this.xhr.statusText !== "canceled" ) { + this._addClass( tab, "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); - $.effects.unshift( element, queuelen, 4 ); -} ); + this.xhr + .done( function( response, status, jqXHR ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + panel.html( response ); + that._trigger( "load", event, eventData ); -/*! - * jQuery UI Effects Highlight 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + complete( jqXHR, status ); + }, 1 ); + } ) + .fail( function( jqXHR, status ) { -//>>label: Highlight Effect -//>>group: Effects -//>>description: Highlights the background of an element in a defined color for a custom duration. -//>>docs: http://api.jqueryui.com/highlight-effect/ -//>>demos: http://jqueryui.com/effect/ + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + complete( jqXHR, status ); + }, 1 ); + } ); + } + }, + _ajaxSettings: function( anchor, event, eventData ) { + var that = this; + return { -var effectsEffectHighlight = $.effects.define( "highlight", "show", function( options, done ) { - var element = $( this ), - animation = { - backgroundColor: element.css( "backgroundColor" ) + // Support: IE <11 only + // Strip any hash that exists to prevent errors with the Ajax request + url: anchor.attr( "href" ).replace( /#.*$/, "" ), + beforeSend: function( jqXHR, settings ) { + return that._trigger( "beforeLoad", event, + $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) ); + } }; - - if ( options.mode === "hide" ) { - animation.opacity = 0; + }, + + _getPanelForTab: function( tab ) { + var id = $( tab ).attr( "aria-controls" ); + return this.element.find( this._sanitizeSelector( "#" + id ) ); } +} ); - $.effects.saveStyle( element ); +// DEPRECATED +// TODO: Switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { - element - .css( { - backgroundImage: "none", - backgroundColor: options.color || "#ffff99" - } ) - .animate( animation, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); -} ); + // Backcompat for ui-tab class (now ui-tabs-tab) + $.widget( "ui.tabs", $.ui.tabs, { + _processTabs: function() { + this._superApply( arguments ); + this._addClass( this.tabs, "ui-tab" ); + } + } ); +} + +var widgetsTabs = $.ui.tabs; /*! - * jQuery UI Effects Size 1.13.1 + * jQuery UI Tooltip 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -18623,433 +18560,501 @@ var effectsEffectHighlight = $.effects.define( "highlight", "show", function( op * http://jquery.org/license */ -//>>label: Size Effect -//>>group: Effects -//>>description: Resize an element to a specified width and height. -//>>docs: http://api.jqueryui.com/size-effect/ -//>>demos: http://jqueryui.com/effect/ - - -var effectsEffectSize = $.effects.define( "size", function( options, done ) { +//>>label: Tooltip +//>>group: Widgets +//>>description: Shows additional information for any element on hover or focus. +//>>docs: http://api.jqueryui.com/tooltip/ +//>>demos: http://jqueryui.com/tooltip/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/tooltip.css +//>>css.theme: ../../themes/base/theme.css - // Create element - var baseline, factor, temp, - element = $( this ), - // Copy for children - cProps = [ "fontSize" ], - vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ], - hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ], +$.widget( "ui.tooltip", { + version: "1.13.2", + options: { + classes: { + "ui-tooltip": "ui-corner-all ui-widget-shadow" + }, + content: function() { + var title = $( this ).attr( "title" ); - // Set options - mode = options.mode, - restore = mode !== "effect", - scale = options.scale || "both", - origin = options.origin || [ "middle", "center" ], - position = element.css( "position" ), - pos = element.position(), - original = $.effects.scaledDimensions( element ), - from = options.from || original, - to = options.to || $.effects.scaledDimensions( element, 0 ); + // Escape title, since we're going from an attribute to raw HTML + return $( "<a>" ).text( title ).html(); + }, + hide: true, - $.effects.createPlaceholder( element ); + // Disabled elements have inconsistent behavior across browsers (#8661) + items: "[title]:not([disabled])", + position: { + my: "left top+15", + at: "left bottom", + collision: "flipfit flip" + }, + show: true, + track: false, - if ( mode === "show" ) { - temp = from; - from = to; - to = temp; - } + // Callbacks + close: null, + open: null + }, - // Set scaling factor - factor = { - from: { - y: from.height / original.height, - x: from.width / original.width - }, - to: { - y: to.height / original.height, - x: to.width / original.width - } - }; + _addDescribedBy: function( elem, id ) { + var describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ); + describedby.push( id ); + elem + .data( "ui-tooltip-id", id ) + .attr( "aria-describedby", String.prototype.trim.call( describedby.join( " " ) ) ); + }, - // Scale the css box - if ( scale === "box" || scale === "both" ) { + _removeDescribedBy: function( elem ) { + var id = elem.data( "ui-tooltip-id" ), + describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ), + index = $.inArray( id, describedby ); - // Vertical props scaling - if ( factor.from.y !== factor.to.y ) { - from = $.effects.setTransition( element, vProps, factor.from.y, from ); - to = $.effects.setTransition( element, vProps, factor.to.y, to ); + if ( index !== -1 ) { + describedby.splice( index, 1 ); } - // Horizontal props scaling - if ( factor.from.x !== factor.to.x ) { - from = $.effects.setTransition( element, hProps, factor.from.x, from ); - to = $.effects.setTransition( element, hProps, factor.to.x, to ); + elem.removeData( "ui-tooltip-id" ); + describedby = String.prototype.trim.call( describedby.join( " " ) ); + if ( describedby ) { + elem.attr( "aria-describedby", describedby ); + } else { + elem.removeAttr( "aria-describedby" ); } - } + }, - // Scale the content - if ( scale === "content" || scale === "both" ) { + _create: function() { + this._on( { + mouseover: "open", + focusin: "open" + } ); - // Vertical props scaling - if ( factor.from.y !== factor.to.y ) { - from = $.effects.setTransition( element, cProps, factor.from.y, from ); - to = $.effects.setTransition( element, cProps, factor.to.y, to ); - } - } + // IDs of generated tooltips, needed for destroy + this.tooltips = {}; - // Adjust the position properties based on the provided origin points - if ( origin ) { - baseline = $.effects.getBaseline( origin, original ); - from.top = ( original.outerHeight - from.outerHeight ) * baseline.y + pos.top; - from.left = ( original.outerWidth - from.outerWidth ) * baseline.x + pos.left; - to.top = ( original.outerHeight - to.outerHeight ) * baseline.y + pos.top; - to.left = ( original.outerWidth - to.outerWidth ) * baseline.x + pos.left; - } - delete from.outerHeight; - delete from.outerWidth; - element.css( from ); + // IDs of parent tooltips where we removed the title attribute + this.parents = {}; - // Animate the children if desired - if ( scale === "content" || scale === "both" ) { + // Append the aria-live region so tooltips announce correctly + this.liveRegion = $( "<div>" ) + .attr( { + role: "log", + "aria-live": "assertive", + "aria-relevant": "additions" + } ) + .appendTo( this.document[ 0 ].body ); + this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" ); - vProps = vProps.concat( [ "marginTop", "marginBottom" ] ).concat( cProps ); - hProps = hProps.concat( [ "marginLeft", "marginRight" ] ); + this.disabledTitles = $( [] ); + }, - // Only animate children with width attributes specified - // TODO: is this right? should we include anything with css width specified as well - element.find( "*[width]" ).each( function() { - var child = $( this ), - childOriginal = $.effects.scaledDimensions( child ), - childFrom = { - height: childOriginal.height * factor.from.y, - width: childOriginal.width * factor.from.x, - outerHeight: childOriginal.outerHeight * factor.from.y, - outerWidth: childOriginal.outerWidth * factor.from.x - }, - childTo = { - height: childOriginal.height * factor.to.y, - width: childOriginal.width * factor.to.x, - outerHeight: childOriginal.height * factor.to.y, - outerWidth: childOriginal.width * factor.to.x - }; + _setOption: function( key, value ) { + var that = this; - // Vertical props scaling - if ( factor.from.y !== factor.to.y ) { - childFrom = $.effects.setTransition( child, vProps, factor.from.y, childFrom ); - childTo = $.effects.setTransition( child, vProps, factor.to.y, childTo ); - } + this._super( key, value ); - // Horizontal props scaling - if ( factor.from.x !== factor.to.x ) { - childFrom = $.effects.setTransition( child, hProps, factor.from.x, childFrom ); - childTo = $.effects.setTransition( child, hProps, factor.to.x, childTo ); - } + if ( key === "content" ) { + $.each( this.tooltips, function( id, tooltipData ) { + that._updateContent( tooltipData.element ); + } ); + } + }, - if ( restore ) { - $.effects.saveStyle( child ); - } + _setOptionDisabled: function( value ) { + this[ value ? "_disable" : "_enable" ](); + }, - // Animate children - child.css( childFrom ); - child.animate( childTo, options.duration, options.easing, function() { + _disable: function() { + var that = this; - // Restore children - if ( restore ) { - $.effects.restoreStyle( child ); - } - } ); + // Close open tooltips + $.each( this.tooltips, function( id, tooltipData ) { + var event = $.Event( "blur" ); + event.target = event.currentTarget = tooltipData.element[ 0 ]; + that.close( event, true ); } ); - } - // Animate - element.animate( to, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: function() { + // Remove title attributes to prevent native tooltips + this.disabledTitles = this.disabledTitles.add( + this.element.find( this.options.items ).addBack() + .filter( function() { + var element = $( this ); + if ( element.is( "[title]" ) ) { + return element + .data( "ui-tooltip-title", element.attr( "title" ) ) + .removeAttr( "title" ); + } + } ) + ); + }, - var offset = element.offset(); + _enable: function() { - if ( to.opacity === 0 ) { - element.css( "opacity", from.opacity ); + // restore title attributes + this.disabledTitles.each( function() { + var element = $( this ); + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); } + } ); + this.disabledTitles = $( [] ); + }, - if ( !restore ) { - element - .css( "position", position === "static" ? "relative" : position ) - .offset( offset ); + open: function( event ) { + var that = this, + target = $( event ? event.target : this.element ) - // Need to save style here so that automatic style restoration - // doesn't restore to the original styles from before the animation. - $.effects.saveStyle( element ); - } + // we need closest here due to mouseover bubbling, + // but always pointing at the same event target + .closest( this.options.items ); - done(); + // No element to show a tooltip for or the tooltip is already open + if ( !target.length || target.data( "ui-tooltip-id" ) ) { + return; } - } ); -} ); + if ( target.attr( "title" ) ) { + target.data( "ui-tooltip-title", target.attr( "title" ) ); + } + target.data( "ui-tooltip-open", true ); -/*! - * jQuery UI Effects Scale 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // Kill parent tooltips, custom or native, for hover + if ( event && event.type === "mouseover" ) { + target.parents().each( function() { + var parent = $( this ), + blurEvent; + if ( parent.data( "ui-tooltip-open" ) ) { + blurEvent = $.Event( "blur" ); + blurEvent.target = blurEvent.currentTarget = this; + that.close( blurEvent, true ); + } + if ( parent.attr( "title" ) ) { + parent.uniqueId(); + that.parents[ this.id ] = { + element: this, + title: parent.attr( "title" ) + }; + parent.attr( "title", "" ); + } + } ); + } -//>>label: Scale Effect -//>>group: Effects -//>>description: Grows or shrinks an element and its content. -//>>docs: http://api.jqueryui.com/scale-effect/ -//>>demos: http://jqueryui.com/effect/ + this._registerCloseHandlers( event, target ); + this._updateContent( target, event ); + }, + _updateContent: function( target, event ) { + var content, + contentOption = this.options.content, + that = this, + eventType = event ? event.type : null; -var effectsEffectScale = $.effects.define( "scale", function( options, done ) { + if ( typeof contentOption === "string" || contentOption.nodeType || + contentOption.jquery ) { + return this._open( event, target, contentOption ); + } - // Create element - var el = $( this ), - mode = options.mode, - percent = parseInt( options.percent, 10 ) || - ( parseInt( options.percent, 10 ) === 0 ? 0 : ( mode !== "effect" ? 0 : 100 ) ), + content = contentOption.call( target[ 0 ], function( response ) { - newOptions = $.extend( true, { - from: $.effects.scaledDimensions( el ), - to: $.effects.scaledDimensions( el, percent, options.direction || "both" ), - origin: options.origin || [ "middle", "center" ] - }, options ); + // IE may instantly serve a cached response for ajax requests + // delay this call to _open so the other call to _open runs first + that._delay( function() { - // Fade option to support puff - if ( options.fade ) { - newOptions.from.opacity = 1; - newOptions.to.opacity = 0; - } + // Ignore async response if tooltip was closed already + if ( !target.data( "ui-tooltip-open" ) ) { + return; + } - $.effects.effect.size.call( this, newOptions, done ); -} ); + // JQuery creates a special event for focusin when it doesn't + // exist natively. To improve performance, the native event + // object is reused and the type is changed. Therefore, we can't + // rely on the type being correct after the event finished + // bubbling, so we set it back to the previous value. (#8740) + if ( event ) { + event.type = eventType; + } + this._open( event, target, response ); + } ); + } ); + if ( content ) { + this._open( event, target, content ); + } + }, + _open: function( event, target, content ) { + var tooltipData, tooltip, delayedShow, a11yContent, + positionOption = $.extend( {}, this.options.position ); -/*! - * jQuery UI Effects Puff 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + if ( !content ) { + return; + } -//>>label: Puff Effect -//>>group: Effects -//>>description: Creates a puff effect by scaling the element up and hiding it at the same time. -//>>docs: http://api.jqueryui.com/puff-effect/ -//>>demos: http://jqueryui.com/effect/ + // Content can be updated multiple times. If the tooltip already + // exists, then just update the content and bail. + tooltipData = this._find( target ); + if ( tooltipData ) { + tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content ); + return; + } + // If we have a title, clear it to prevent the native tooltip + // we have to check first to avoid defining a title if none exists + // (we don't want to cause an element to start matching [title]) + // + // We use removeAttr only for key events, to allow IE to export the correct + // accessible attributes. For mouse events, set to empty string to avoid + // native tooltip showing up (happens only when removing inside mouseover). + if ( target.is( "[title]" ) ) { + if ( event && event.type === "mouseover" ) { + target.attr( "title", "" ); + } else { + target.removeAttr( "title" ); + } + } -var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, done ) { - var newOptions = $.extend( true, {}, options, { - fade: true, - percent: parseInt( options.percent, 10 ) || 150 - } ); + tooltipData = this._tooltip( target ); + tooltip = tooltipData.tooltip; + this._addDescribedBy( target, tooltip.attr( "id" ) ); + tooltip.find( ".ui-tooltip-content" ).html( content ); - $.effects.effect.scale.call( this, newOptions, done ); -} ); + // Support: Voiceover on OS X, JAWS on IE <= 9 + // JAWS announces deletions even when aria-relevant="additions" + // Voiceover will sometimes re-read the entire log region's contents from the beginning + this.liveRegion.children().hide(); + a11yContent = $( "<div>" ).html( tooltip.find( ".ui-tooltip-content" ).html() ); + a11yContent.removeAttr( "name" ).find( "[name]" ).removeAttr( "name" ); + a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" ); + a11yContent.appendTo( this.liveRegion ); + function position( event ) { + positionOption.of = event; + if ( tooltip.is( ":hidden" ) ) { + return; + } + tooltip.position( positionOption ); + } + if ( this.options.track && event && /^mouse/.test( event.type ) ) { + this._on( this.document, { + mousemove: position + } ); -/*! - * jQuery UI Effects Pulsate 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // trigger once to override element-relative positioning + position( event ); + } else { + tooltip.position( $.extend( { + of: target + }, this.options.position ) ); + } -//>>label: Pulsate Effect -//>>group: Effects -//>>description: Pulsates an element n times by changing the opacity to zero and back. -//>>docs: http://api.jqueryui.com/pulsate-effect/ -//>>demos: http://jqueryui.com/effect/ + tooltip.hide(); + this._show( tooltip, this.options.show ); -var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( options, done ) { - var element = $( this ), - mode = options.mode, - show = mode === "show", - hide = mode === "hide", - showhide = show || hide, + // Handle tracking tooltips that are shown with a delay (#8644). As soon + // as the tooltip is visible, position the tooltip using the most recent + // event. + // Adds the check to add the timers only when both delay and track options are set (#14682) + if ( this.options.track && this.options.show && this.options.show.delay ) { + delayedShow = this.delayedShow = setInterval( function() { + if ( tooltip.is( ":visible" ) ) { + position( positionOption.of ); + clearInterval( delayedShow ); + } + }, 13 ); + } - // Showing or hiding leaves off the "last" animation - anims = ( ( options.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ), - duration = options.duration / anims, - animateTo = 0, - i = 1, - queuelen = element.queue().length; + this._trigger( "open", event, { tooltip: tooltip } ); + }, - if ( show || !element.is( ":visible" ) ) { - element.css( "opacity", 0 ).show(); - animateTo = 1; - } + _registerCloseHandlers: function( event, target ) { + var events = { + keyup: function( event ) { + if ( event.keyCode === $.ui.keyCode.ESCAPE ) { + var fakeEvent = $.Event( event ); + fakeEvent.currentTarget = target[ 0 ]; + this.close( fakeEvent, true ); + } + } + }; + + // Only bind remove handler for delegated targets. Non-delegated + // tooltips will handle this in destroy. + if ( target[ 0 ] !== this.element[ 0 ] ) { + events.remove = function() { + var targetElement = this._find( target ); + if ( targetElement ) { + this._removeTooltip( targetElement.tooltip ); + } + }; + } - // Anims - 1 opacity "toggles" - for ( ; i < anims; i++ ) { - element.animate( { opacity: animateTo }, duration, options.easing ); - animateTo = 1 - animateTo; - } + if ( !event || event.type === "mouseover" ) { + events.mouseleave = "close"; + } + if ( !event || event.type === "focusin" ) { + events.focusout = "close"; + } + this._on( true, target, events ); + }, - element.animate( { opacity: animateTo }, duration, options.easing ); + close: function( event ) { + var tooltip, + that = this, + target = $( event ? event.currentTarget : this.element ), + tooltipData = this._find( target ); - element.queue( done ); + // The tooltip may already be closed + if ( !tooltipData ) { - $.effects.unshift( element, queuelen, anims + 1 ); -} ); + // We set ui-tooltip-open immediately upon open (in open()), but only set the + // additional data once there's actually content to show (in _open()). So even if the + // tooltip doesn't have full data, we always remove ui-tooltip-open in case we're in + // the period between open() and _open(). + target.removeData( "ui-tooltip-open" ); + return; + } + tooltip = tooltipData.tooltip; -/*! - * jQuery UI Effects Shake 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // Disabling closes the tooltip, so we need to track when we're closing + // to avoid an infinite loop in case the tooltip becomes disabled on close + if ( tooltipData.closing ) { + return; + } -//>>label: Shake Effect -//>>group: Effects -//>>description: Shakes an element horizontally or vertically n times. -//>>docs: http://api.jqueryui.com/shake-effect/ -//>>demos: http://jqueryui.com/effect/ + // Clear the interval for delayed tracking tooltips + clearInterval( this.delayedShow ); + // Only set title if we had one before (see comment in _open()) + // If the title attribute has changed since open(), don't restore + if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) { + target.attr( "title", target.data( "ui-tooltip-title" ) ); + } -var effectsEffectShake = $.effects.define( "shake", function( options, done ) { + this._removeDescribedBy( target ); - var i = 1, - element = $( this ), - direction = options.direction || "left", - distance = options.distance || 20, - times = options.times || 3, - anims = times * 2 + 1, - speed = Math.round( options.duration / anims ), - ref = ( direction === "up" || direction === "down" ) ? "top" : "left", - positiveMotion = ( direction === "up" || direction === "left" ), - animation = {}, - animation1 = {}, - animation2 = {}, + tooltipData.hiding = true; + tooltip.stop( true ); + this._hide( tooltip, this.options.hide, function() { + that._removeTooltip( $( this ) ); + } ); - queuelen = element.queue().length; + target.removeData( "ui-tooltip-open" ); + this._off( target, "mouseleave focusout keyup" ); - $.effects.createPlaceholder( element ); + // Remove 'remove' binding only on delegated targets + if ( target[ 0 ] !== this.element[ 0 ] ) { + this._off( target, "remove" ); + } + this._off( this.document, "mousemove" ); - // Animation - animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance; - animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2; - animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2; + if ( event && event.type === "mouseleave" ) { + $.each( this.parents, function( id, parent ) { + $( parent.element ).attr( "title", parent.title ); + delete that.parents[ id ]; + } ); + } - // Animate - element.animate( animation, speed, options.easing ); + tooltipData.closing = true; + this._trigger( "close", event, { tooltip: tooltip } ); + if ( !tooltipData.hiding ) { + tooltipData.closing = false; + } + }, - // Shakes - for ( ; i < times; i++ ) { - element - .animate( animation1, speed, options.easing ) - .animate( animation2, speed, options.easing ); - } + _tooltip: function( element ) { + var tooltip = $( "<div>" ).attr( "role", "tooltip" ), + content = $( "<div>" ).appendTo( tooltip ), + id = tooltip.uniqueId().attr( "id" ); - element - .animate( animation1, speed, options.easing ) - .animate( animation, speed / 2, options.easing ) - .queue( done ); + this._addClass( content, "ui-tooltip-content" ); + this._addClass( tooltip, "ui-tooltip", "ui-widget ui-widget-content" ); - $.effects.unshift( element, queuelen, anims + 1 ); -} ); + tooltip.appendTo( this._appendTo( element ) ); + return this.tooltips[ id ] = { + element: element, + tooltip: tooltip + }; + }, -/*! - * jQuery UI Effects Slide 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + _find: function( target ) { + var id = target.data( "ui-tooltip-id" ); + return id ? this.tooltips[ id ] : null; + }, -//>>label: Slide Effect -//>>group: Effects -//>>description: Slides an element in and out of the viewport. -//>>docs: http://api.jqueryui.com/slide-effect/ -//>>demos: http://jqueryui.com/effect/ + _removeTooltip: function( tooltip ) { + // Clear the interval for delayed tracking tooltips + clearInterval( this.delayedShow ); -var effectsEffectSlide = $.effects.define( "slide", "show", function( options, done ) { - var startClip, startRef, - element = $( this ), - map = { - up: [ "bottom", "top" ], - down: [ "top", "bottom" ], - left: [ "right", "left" ], - right: [ "left", "right" ] - }, - mode = options.mode, - direction = options.direction || "left", - ref = ( direction === "up" || direction === "down" ) ? "top" : "left", - positiveMotion = ( direction === "up" || direction === "left" ), - distance = options.distance || - element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ), - animation = {}; + tooltip.remove(); + delete this.tooltips[ tooltip.attr( "id" ) ]; + }, - $.effects.createPlaceholder( element ); + _appendTo: function( target ) { + var element = target.closest( ".ui-front, dialog" ); - startClip = element.cssClip(); - startRef = element.position()[ ref ]; + if ( !element.length ) { + element = this.document[ 0 ].body; + } - // Define hide animation - animation[ ref ] = ( positiveMotion ? -1 : 1 ) * distance + startRef; - animation.clip = element.cssClip(); - animation.clip[ map[ direction ][ 1 ] ] = animation.clip[ map[ direction ][ 0 ] ]; + return element; + }, - // Reverse the animation if we're showing - if ( mode === "show" ) { - element.cssClip( animation.clip ); - element.css( ref, animation[ ref ] ); - animation.clip = startClip; - animation[ ref ] = startRef; - } + _destroy: function() { + var that = this; - // Actually animate - element.animate( animation, { - queue: false, - duration: options.duration, - easing: options.easing, - complete: done - } ); -} ); + // Close open tooltips + $.each( this.tooltips, function( id, tooltipData ) { + // Delegate to close method to handle common cleanup + var event = $.Event( "blur" ), + element = tooltipData.element; + event.target = event.currentTarget = element[ 0 ]; + that.close( event, true ); -/*! - * jQuery UI Effects Transfer 1.13.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ + // Remove immediately; destroying an open tooltip doesn't use the + // hide animation + $( "#" + id ).remove(); -//>>label: Transfer Effect -//>>group: Effects -//>>description: Displays a transfer effect from one element to another. -//>>docs: http://api.jqueryui.com/transfer-effect/ -//>>demos: http://jqueryui.com/effect/ + // Restore the title + if ( element.data( "ui-tooltip-title" ) ) { + // If the title attribute has changed since open(), don't restore + if ( !element.attr( "title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + } + element.removeData( "ui-tooltip-title" ); + } + } ); + this.liveRegion.remove(); + } +} ); -var effect; +// DEPRECATED +// TODO: Switch return back to widget declaration at top of file when this is removed if ( $.uiBackCompat !== false ) { - effect = $.effects.define( "transfer", function( options, done ) { - $( this ).transfer( options, done ); + + // Backcompat for tooltipClass option + $.widget( "ui.tooltip", $.ui.tooltip, { + options: { + tooltipClass: null + }, + _tooltip: function() { + var tooltipData = this._superApply( arguments ); + if ( this.options.tooltipClass ) { + tooltipData.tooltip.addClass( this.options.tooltipClass ); + } + return tooltipData; + } } ); } -var effectsEffectTransfer = effect; + +var widgetsTooltip = $.ui.tooltip; diff --git a/lib/web/jquery/jquery.validate.js b/lib/web/jquery/jquery.validate.js index b9bc14f55e393..dcaf4fe918f94 100644 --- a/lib/web/jquery/jquery.validate.js +++ b/lib/web/jquery/jquery.validate.js @@ -1,9 +1,9 @@ /*! - * jQuery Validation Plugin v1.19.3 + * jQuery Validation Plugin v1.19.5 * * https://jqueryvalidation.org/ * - * Copyright (c) 2021 Jörn Zaefferer + * Copyright (c) 2022 Jörn Zaefferer * Released under the MIT license */ (function( factory ) { @@ -378,7 +378,6 @@ dateISO: "Please enter a valid date (ISO).", number: "Please enter a valid number.", digits: "Please enter only digits.", - creditcard: "Please enter a valid credit card number.", equalTo: "Please enter the same value again.", maxlength: $.validator.format( "Please enter no more than {0} characters." ), minlength: $.validator.format( "Please enter at least {0} characters." ), @@ -784,7 +783,7 @@ normalizer = this.settings.normalizer; } - // If normalizer is defined, then call it to retrieve the changed value instead + // If normalizer is defined, then call it to the changed value instead // of using the real one. // Note that `this` in the normalizer is `element`. if ( normalizer ) { @@ -875,13 +874,13 @@ } var message = this.findDefined( - this.customMessage( element.name, rule.method ), - this.customDataMessage( element, rule.method ), + this.customMessage( element.name, rule.method ), + this.customDataMessage( element, rule.method ), - // 'title' is never undefined, so handle empty string as undefined - !this.settings.ignoreTitle && element.title || undefined, - $.validator.messages[ rule.method ], - "<strong>Warning: No message defined for " + element.name + "</strong>" + // 'title' is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[ rule.method ], + "<strong>Warning: No message defined for " + element.name + "</strong>" ), theregex = /\$?\{(\d+)\}/g; if ( typeof message === "function" ) { @@ -982,7 +981,7 @@ if ( this.labelContainer.length ) { this.labelContainer.append( place ); } else if ( this.settings.errorPlacement ) { - this.settings.errorPlacement( place, $( element ) ); + this.settings.errorPlacement.call( this, place, $( element ) ); } else { place.insertAfter( element ); } @@ -1052,7 +1051,11 @@ // meta-characters that should be escaped in order to be used with JQuery // as a literal part of a name/id or any selector. escapeCssMeta: function( string ) { - return (string || '').replace( /([\\!"#$%&'()*+,./:;<=>?@\[\]^`{|}~])/g, "\\$1" ); + if ( string === undefined ) { + return ""; + } + + return string.replace( /([\\!"#$%&'()*+,./:;<=>?@\[\]^`{|}~])/g, "\\$1" ); }, idOrName: function( element ) { @@ -1128,8 +1131,8 @@ } delete this.pending[ element.name ]; $( element ).removeClass( this.settings.pendingClass ); - if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { - $( this.currentForm ).submit(); + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() && this.pendingRequest === 0 ) { + $( this.currentForm ).trigger( "submit" ); // Remove the hidden input that was used as a replacement for the // missing submit button. The hidden input is added by `handle()` @@ -1234,7 +1237,7 @@ // Exception: the jquery validate 'range' method // does not test for the html5 'range' type - rules[ method ] = true; + rules[ type === "date" ? "dateISO" : method ] = true; } }, @@ -1283,7 +1286,7 @@ $(element).metadata()[meta] : $(element).metadata(); }, - + dataRules: function( element ) { var rules = {}, $element = $( element ), @@ -1443,7 +1446,7 @@ // https://gist.github.com/dperini/729294 // see also https://mathiasbynens.be/demo/url-regex // modified to allow protocol-relative URLs - return this.optional( element ) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( value ); + return this.optional( element ) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[^\]\[?\/<~#`!@$^&*()+=}|:";',>{ ]|%[0-9A-Fa-f]{2})+(?::(?:[^\]\[?\/<~#`!@$^&*()+=}|:";',>{ ]|%[0-9A-Fa-f]{2})*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( value ); }, // https://jqueryvalidation.org/date-method/ diff --git a/lib/web/magnifier/magnifier.js b/lib/web/magnifier/magnifier.js index 29726688f4d5f..8b525b2e047b8 100644 --- a/lib/web/magnifier/magnifier.js +++ b/lib/web/magnifier/magnifier.js @@ -541,11 +541,9 @@ showWrapper = false; $(thumbObj).on('load', function () { - if (data.length > 0) { + if (data.hasOwnProperty(idx)) { data[idx].status = 1; - $(largeObj).on('load', function () { - if (largeObj.width > largeWrapper.width() || largeObj.height > largeWrapper.height()) { showWrapper = true; bindEvents(eventType, thumb); @@ -559,7 +557,6 @@ updateLensOnLoad(idx, thumb, largeObj, largeWrapper); } }); - largeObj.src = data[idx].largeUrl; } }); diff --git a/lib/web/magnifier/magnify.js b/lib/web/magnifier/magnify.js index 7d193fc1cd970..f5cc2114a3f12 100644 --- a/lib/web/magnifier/magnify.js +++ b/lib/web/magnifier/magnify.js @@ -505,7 +505,7 @@ define([ if (!$fotoramaStage.hasClass('magnify-wheel-loaded')) { if (fotoramaStage && fotoramaStage.addEventListener) { if ('onwheel' in document) { - fotoramaStage.addEventListener('wheel', onWheel, { passive: true }); + fotoramaStage.addEventListener('wheel', onWheel, { passive: false }); } else if ('onmousewheel' in document) { fotoramaStage.addEventListener('mousewheel', onWheel); } else { diff --git a/lib/web/moment.js b/lib/web/moment.js index aa5cc383efc66..498eeefe841c8 100644 --- a/lib/web/moment.js +++ b/lib/web/moment.js @@ -1,6 +1,7 @@ //! moment.js -//! version : 2.29.1 +//! version : 2.29.4 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function f(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function l(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(m(e,t))return;return 1}function r(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function a(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function d(e,t){for(var n=[],s=0;s<e.length;++s)n.push(t(e[s],s));return n}function c(e,t){for(var n in t)m(t,n)&&(e[n]=t[n]);return m(t,"toString")&&(e.toString=t.toString),m(t,"valueOf")&&(e.valueOf=t.valueOf),e}function _(e,t,n,s){return xt(e,t,n,s,!0).utc()}function y(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function g(e){if(null==e._isValid){var t=y(e),n=i.call(t.parsedDateParts,function(e){return null!=e}),s=!isNaN(e._d.getTime())&&t.overflow<0&&!t.empty&&!t.invalidEra&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n);if(e._strict&&(s=s&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour),null!=Object.isFrozen&&Object.isFrozen(e))return s;e._isValid=s}return e._isValid}function w(e){var t=_(NaN);return null!=e?c(y(t),e):y(t).userInvalidated=!0,t}i=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,s=0;s<n;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1};var p=f.momentProperties=[],t=!1;function v(e,t){var n,s,i;if(r(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),r(t._i)||(e._i=t._i),r(t._f)||(e._f=t._f),r(t._l)||(e._l=t._l),r(t._strict)||(e._strict=t._strict),r(t._tzm)||(e._tzm=t._tzm),r(t._isUTC)||(e._isUTC=t._isUTC),r(t._offset)||(e._offset=t._offset),r(t._pf)||(e._pf=y(t)),r(t._locale)||(e._locale=t._locale),0<p.length)for(n=0;n<p.length;n++)r(i=t[s=p[n]])||(e[s]=i);return e}function k(e){v(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===t&&(t=!0,f.updateOffset(this),t=!1)}function M(e){return e instanceof k||null!=e&&null!=e._isAMomentObject}function D(e){!1===f.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function n(i,r){var a=!0;return c(function(){if(null!=f.deprecationHandler&&f.deprecationHandler(null,i),a){for(var e,t,n=[],s=0;s<arguments.length;s++){if(e="","object"==typeof arguments[s]){for(t in e+="\n["+s+"] ",arguments[0])m(arguments[0],t)&&(e+=t+": "+arguments[0][t]+", ");e=e.slice(0,-2)}else e=arguments[s];n.push(e)}D(i+"\nArguments: "+Array.prototype.slice.call(n).join("")+"\n"+(new Error).stack),a=!1}return r.apply(this,arguments)},r)}var s,S={};function Y(e,t){null!=f.deprecationHandler&&f.deprecationHandler(e,t),S[e]||(D(t),S[e]=!0)}function O(e){return"undefined"!=typeof Function&&e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function b(e,t){var n,s=c({},e);for(n in t)m(t,n)&&(u(e[n])&&u(t[n])?(s[n]={},c(s[n],e[n]),c(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)m(e,n)&&!m(t,n)&&u(e[n])&&(s[n]=c({},s[n]));return s}function x(e){null!=e&&this.set(e)}f.suppressDeprecationWarnings=!1,f.deprecationHandler=null,s=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)m(e,t)&&n.push(t);return n};function T(e,t,n){var s=""+Math.abs(e),i=t-s.length;return(0<=e?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+s}var N=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,P=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,R={},W={};function C(e,t,n,s){var i="string"==typeof s?function(){return this[s]()}:s;e&&(W[e]=i),t&&(W[t[0]]=function(){return T(i.apply(this,arguments),t[1],t[2])}),n&&(W[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function U(e,t){return e.isValid()?(t=H(t,e.localeData()),R[t]=R[t]||function(s){for(var e,i=s.match(N),t=0,r=i.length;t<r;t++)W[i[t]]?i[t]=W[i[t]]:i[t]=(e=i[t]).match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"");return function(e){for(var t="",n=0;n<r;n++)t+=O(i[n])?i[n].call(e,s):i[n];return t}}(t),R[t](e)):e.localeData().invalidDate()}function H(e,t){var n=5;function s(e){return t.longDateFormat(e)||e}for(P.lastIndex=0;0<=n&&P.test(e);)e=e.replace(P,s),P.lastIndex=0,--n;return e}var F={};function L(e,t){var n=e.toLowerCase();F[n]=F[n+"s"]=F[t]=e}function V(e){return"string"==typeof e?F[e]||F[e.toLowerCase()]:void 0}function G(e){var t,n,s={};for(n in e)m(e,n)&&(t=V(n))&&(s[t]=e[n]);return s}var E={};function A(e,t){E[e]=t}function j(e){return e%4==0&&e%100!=0||e%400==0}function I(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function Z(e){var t=+e,n=0;return 0!=t&&isFinite(t)&&(n=I(t)),n}function z(t,n){return function(e){return null!=e?(q(this,t,e),f.updateOffset(this,n),this):$(this,t)}}function $(e,t){return e.isValid()?e._d["get"+(e._isUTC?"UTC":"")+t]():NaN}function q(e,t,n){e.isValid()&&!isNaN(n)&&("FullYear"===t&&j(e.year())&&1===e.month()&&29===e.date()?(n=Z(n),e._d["set"+(e._isUTC?"UTC":"")+t](n,e.month(),xe(n,e.month()))):e._d["set"+(e._isUTC?"UTC":"")+t](n))}var B,J=/\d/,Q=/\d\d/,X=/\d{3}/,K=/\d{4}/,ee=/[+-]?\d{6}/,te=/\d\d?/,ne=/\d\d\d\d?/,se=/\d\d\d\d\d\d?/,ie=/\d{1,3}/,re=/\d{1,4}/,ae=/[+-]?\d{1,6}/,oe=/\d+/,ue=/[+-]?\d+/,le=/Z|[+-]\d\d:?\d\d/gi,he=/Z|[+-]\d\d(?::?\d\d)?/gi,de=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i;function ce(e,n,s){B[e]=O(n)?n:function(e,t){return e&&s?s:n}}function fe(e,t){return m(B,e)?B[e](t._strict,t._locale):new RegExp(me(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,t,n,s,i){return t||n||s||i})))}function me(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}B={};var _e={};function ye(e,n){var t,s=n;for("string"==typeof e&&(e=[e]),h(n)&&(s=function(e,t){t[n]=Z(e)}),t=0;t<e.length;t++)_e[e[t]]=s}function ge(e,i){ye(e,function(e,t,n,s){n._w=n._w||{},i(e,n._w,n,s)})}var we,pe=0,ve=1,ke=2,Me=3,De=4,Se=5,Ye=6,Oe=7,be=8;function xe(e,t){if(isNaN(e)||isNaN(t))return NaN;var n,s=(t%(n=12)+n)%n;return e+=(t-s)/12,1==s?j(e)?29:28:31-s%7%2}we=Array.prototype.indexOf?Array.prototype.indexOf:function(e){for(var t=0;t<this.length;++t)if(this[t]===e)return t;return-1},C("M",["MM",2],"Mo",function(){return this.month()+1}),C("MMM",0,0,function(e){return this.localeData().monthsShort(this,e)}),C("MMMM",0,0,function(e){return this.localeData().months(this,e)}),L("month","M"),A("month",8),ce("M",te),ce("MM",te,Q),ce("MMM",function(e,t){return t.monthsShortRegex(e)}),ce("MMMM",function(e,t){return t.monthsRegex(e)}),ye(["M","MM"],function(e,t){t[ve]=Z(e)-1}),ye(["MMM","MMMM"],function(e,t,n,s){var i=n._locale.monthsParse(e,s,n._strict);null!=i?t[ve]=i:y(n).invalidMonth=e});var Te="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Ne="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),Pe=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Re=de,We=de;function Ce(e,t){var n;if(!e.isValid())return e;if("string"==typeof t)if(/^\d+$/.test(t))t=Z(t);else if(!h(t=e.localeData().monthsParse(t)))return e;return n=Math.min(e.date(),xe(e.year(),t)),e._d["set"+(e._isUTC?"UTC":"")+"Month"](t,n),e}function Ue(e){return null!=e?(Ce(this,e),f.updateOffset(this,!0),this):$(this,"Month")}function He(){function e(e,t){return t.length-e.length}for(var t,n=[],s=[],i=[],r=0;r<12;r++)t=_([2e3,r]),n.push(this.monthsShort(t,"")),s.push(this.months(t,"")),i.push(this.months(t,"")),i.push(this.monthsShort(t,""));for(n.sort(e),s.sort(e),i.sort(e),r=0;r<12;r++)n[r]=me(n[r]),s[r]=me(s[r]);for(r=0;r<24;r++)i[r]=me(i[r]);this._monthsRegex=new RegExp("^("+i.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+n.join("|")+")","i")}function Fe(e){return j(e)?366:365}C("Y",0,0,function(){var e=this.year();return e<=9999?T(e,4):"+"+e}),C(0,["YY",2],0,function(){return this.year()%100}),C(0,["YYYY",4],0,"year"),C(0,["YYYYY",5],0,"year"),C(0,["YYYYYY",6,!0],0,"year"),L("year","y"),A("year",1),ce("Y",ue),ce("YY",te,Q),ce("YYYY",re,K),ce("YYYYY",ae,ee),ce("YYYYYY",ae,ee),ye(["YYYYY","YYYYYY"],pe),ye("YYYY",function(e,t){t[pe]=2===e.length?f.parseTwoDigitYear(e):Z(e)}),ye("YY",function(e,t){t[pe]=f.parseTwoDigitYear(e)}),ye("Y",function(e,t){t[pe]=parseInt(e,10)}),f.parseTwoDigitYear=function(e){return Z(e)+(68<Z(e)?1900:2e3)};var Le=z("FullYear",!0);function Ve(e){var t,n;return e<100&&0<=e?((n=Array.prototype.slice.call(arguments))[0]=e+400,t=new Date(Date.UTC.apply(null,n)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)):t=new Date(Date.UTC.apply(null,arguments)),t}function Ge(e,t,n){var s=7+t-n;return s-(7+Ve(e,0,s).getUTCDay()-t)%7-1}function Ee(e,t,n,s,i){var r,a=1+7*(t-1)+(7+n-s)%7+Ge(e,s,i),o=a<=0?Fe(r=e-1)+a:a>Fe(e)?(r=e+1,a-Fe(e)):(r=e,a);return{year:r,dayOfYear:o}}function Ae(e,t,n){var s,i,r=Ge(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+je(i=e.year()-1,t,n):a>je(e.year(),t,n)?(s=a-je(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function je(e,t,n){var s=Ge(e,t,n),i=Ge(e+1,t,n);return(Fe(e)-s+i)/7}C("w",["ww",2],"wo","week"),C("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),A("week",5),A("isoWeek",5),ce("w",te),ce("ww",te,Q),ce("W",te),ce("WW",te,Q),ge(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=Z(e)});function Ie(e,t){return e.slice(t,7).concat(e.slice(0,t))}C("d",0,"do","day"),C("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),C("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),C("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),C("e",0,0,"weekday"),C("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),A("day",11),A("weekday",11),A("isoWeekday",11),ce("d",te),ce("e",te),ce("E",te),ce("dd",function(e,t){return t.weekdaysMinRegex(e)}),ce("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ce("dddd",function(e,t){return t.weekdaysRegex(e)}),ge(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:y(n).invalidWeekday=e}),ge(["d","e","E"],function(e,t,n,s){t[s]=Z(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$e="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),qe=de,Be=de,Je=de;function Qe(){function e(e,t){return t.length-e.length}for(var t,n,s,i,r=[],a=[],o=[],u=[],l=0;l<7;l++)t=_([2e3,1]).day(l),n=me(this.weekdaysMin(t,"")),s=me(this.weekdaysShort(t,"")),i=me(this.weekdays(t,"")),r.push(n),a.push(s),o.push(i),u.push(n),u.push(s),u.push(i);r.sort(e),a.sort(e),o.sort(e),u.sort(e),this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){C(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}C("H",["HH",2],0,"hour"),C("h",["hh",2],0,Xe),C("k",["kk",2],0,function(){return this.hours()||24}),C("hmm",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)}),C("hmmss",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),C("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),C("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),L("hour","h"),A("hour",13),ce("a",et),ce("A",et),ce("H",te),ce("h",te),ce("k",te),ce("HH",te,Q),ce("hh",te,Q),ce("kk",te,Q),ce("hmm",ne),ce("hmmss",se),ce("Hmm",ne),ce("Hmmss",se),ye(["H","HH"],Me),ye(["k","kk"],function(e,t,n){var s=Z(e);t[Me]=24===s?0:s}),ye(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ye(["h","hh"],function(e,t,n){t[Me]=Z(e),y(n).bigHour=!0}),ye("hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s)),y(n).bigHour=!0}),ye("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i)),y(n).bigHour=!0}),ye("Hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s))}),ye("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i))});var tt=z("Hours",!0);var nt,st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Te,monthsShort:Ne,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){for(var t,n,s,i,r=0;r<e.length;){for(t=(i=at(e[r]).split("-")).length,n=(n=at(e[r+1]))?n.split("-"):null;0<t;){if(s=ut(i.slice(0,t).join("-")))return s;if(n&&n.length>=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s<n;s+=1)if(e[s]!==t[s])return s;return n}(i,n)>=t-1)break;t--}r++}return nt}function ut(t){var e;if(void 0===it[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=nt._abbr,require("./locale/"+t),lt(e)}catch(e){it[t]=null}return it[t]}function lt(e,t){var n;return e&&((n=r(t)?dt(e):ht(e,t))?nt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),nt._abbr}function ht(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])Y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ut(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new x(b(s,t)),rt[e]&&rt[e].forEach(function(e){ht(e.name,e.config)}),lt(e),it[e]}function dt(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return nt;if(!o(e)){if(t=ut(e))return t;e=[e]}return ot(e)}function ct(e){var t,n=e._a;return n&&-2===y(e).overflow&&(t=n[ve]<0||11<n[ve]?ve:n[ke]<1||n[ke]>xe(n[pe],n[ve])?ke:n[Me]<0||24<n[Me]||24===n[Me]&&(0!==n[De]||0!==n[Se]||0!==n[Ye])?Me:n[De]<0||59<n[De]?De:n[Se]<0||59<n[Se]?Se:n[Ye]<0||999<n[Ye]?Ye:-1,y(e)._overflowDayOfYear&&(t<pe||ke<t)&&(t=ke),y(e)._overflowWeeks&&-1===t&&(t=Oe),y(e)._overflowWeekday&&-1===t&&(t=be),y(e).overflow=t),e}var ft=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_t=/Z|[+-]\d\d(?::?\d\d)?/,yt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],gt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],wt=/^\/?Date\((-?\d+)/i,pt=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,vt={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function kt(e){var t,n,s,i,r,a,o=e._i,u=ft.exec(o)||mt.exec(o);if(u){for(y(e).iso=!0,t=0,n=yt.length;t<n;t++)if(yt[t][1].exec(u[1])){i=yt[t][0],s=!1!==yt[t][2];break}if(null==i)return void(e._isValid=!1);if(u[3]){for(t=0,n=gt.length;t<n;t++)if(gt[t][1].exec(u[3])){r=(u[2]||" ")+gt[t][0];break}if(null==r)return void(e._isValid=!1)}if(!s&&null!=r)return void(e._isValid=!1);if(u[4]){if(!_t.exec(u[4]))return void(e._isValid=!1);a="Z"}e._f=i+(r||"")+(a||""),Ot(e)}else e._isValid=!1}function Mt(e,t,n,s,i,r){var a=[function(e){var t=parseInt(e,10);{if(t<=49)return 2e3+t;if(t<=999)return 1900+t}return t}(e),Ne.indexOf(t),parseInt(n,10),parseInt(s,10),parseInt(i,10)];return r&&a.push(parseInt(r,10)),a}function Dt(e){var t,n,s,i,r=pt.exec(e._i.replace(/\([^)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,""));if(r){if(t=Mt(r[4],r[3],r[2],r[5],r[6],r[7]),n=r[1],s=t,i=e,n&&ze.indexOf(n)!==new Date(s[0],s[1],s[2]).getDay()&&(y(i).weekdayMismatch=!0,!void(i._isValid=!1)))return;e._a=t,e._tzm=function(e,t,n){if(e)return vt[e];if(t)return 0;var s=parseInt(n,10),i=s%100;return 60*((s-i)/100)+i}(r[8],r[9],r[10]),e._d=Ve.apply(null,e._a),e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),y(e).rfc2822=!0}else e._isValid=!1}function St(e,t,n){return null!=e?e:null!=t?t:n}function Yt(e){var t,n,s,i,r,a,o,u=[];if(!e._d){for(a=e,o=new Date(f.now()),s=a._useUTC?[o.getUTCFullYear(),o.getUTCMonth(),o.getUTCDate()]:[o.getFullYear(),o.getMonth(),o.getDate()],e._w&&null==e._a[ke]&&null==e._a[ve]&&function(e){var t,n,s,i,r,a,o,u,l;null!=(t=e._w).GG||null!=t.W||null!=t.E?(r=1,a=4,n=St(t.GG,e._a[pe],Ae(Tt(),1,4).year),s=St(t.W,1),((i=St(t.E,1))<1||7<i)&&(u=!0)):(r=e._locale._week.dow,a=e._locale._week.doy,l=Ae(Tt(),r,a),n=St(t.gg,e._a[pe],l.year),s=St(t.w,l.week),null!=t.d?((i=t.d)<0||6<i)&&(u=!0):null!=t.e?(i=t.e+r,(t.e<0||6<t.e)&&(u=!0)):i=r);s<1||s>je(n,r,a)?y(e)._overflowWeeks=!0:null!=u?y(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[pe]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=St(e._a[pe],s[pe]),(e._dayOfYear>Fe(r)||0===e._dayOfYear)&&(y(e)._overflowDayOfYear=!0),n=Ve(r,0,e._dayOfYear),e._a[ve]=n.getUTCMonth(),e._a[ke]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=u[t]=s[t];for(;t<7;t++)e._a[t]=u[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[Me]&&0===e._a[De]&&0===e._a[Se]&&0===e._a[Ye]&&(e._nextDay=!0,e._a[Me]=0),e._d=(e._useUTC?Ve:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,u),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Me]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(y(e).weekdayMismatch=!0)}}function Ot(e){if(e._f!==f.ISO_8601)if(e._f!==f.RFC_2822){e._a=[],y(e).empty=!0;for(var t,n,s,i,r,a,o,u=""+e._i,l=u.length,h=0,d=H(e._f,e._locale).match(N)||[],c=0;c<d.length;c++)n=d[c],(t=(u.match(fe(n,e))||[])[0])&&(0<(s=u.substr(0,u.indexOf(t))).length&&y(e).unusedInput.push(s),u=u.slice(u.indexOf(t)+t.length),h+=t.length),W[n]?(t?y(e).empty=!1:y(e).unusedTokens.push(n),r=n,o=e,null!=(a=t)&&m(_e,r)&&_e[r](a,o._a,o,r)):e._strict&&!t&&y(e).unusedTokens.push(n);y(e).charsLeftOver=l-h,0<u.length&&y(e).unusedInput.push(u),e._a[Me]<=12&&!0===y(e).bigHour&&0<e._a[Me]&&(y(e).bigHour=void 0),y(e).parsedDateParts=e._a.slice(0),y(e).meridiem=e._meridiem,e._a[Me]=function(e,t,n){var s;if(null==n)return t;return null!=e.meridiemHour?e.meridiemHour(t,n):(null!=e.isPM&&((s=e.isPM(n))&&t<12&&(t+=12),s||12!==t||(t=0)),t)}(e._locale,e._a[Me],e._meridiem),null!==(i=y(e).era)&&(e._a[pe]=e._locale.erasConvertYear(i,e._a[pe])),Yt(e),ct(e)}else Dt(e);else kt(e)}function bt(e){var t,n,s=e._i,i=e._f;return e._locale=e._locale||dt(e._l),null===s||void 0===i&&""===s?w({nullInput:!0}):("string"==typeof s&&(e._i=s=e._locale.preparse(s)),M(s)?new k(ct(s)):(a(s)?e._d=s:o(i)?function(e){var t,n,s,i,r,a,o=!1;if(0===e._f.length)return y(e).invalidFormat=!0,e._d=new Date(NaN);for(i=0;i<e._f.length;i++)r=0,a=!1,t=v({},e),null!=e._useUTC&&(t._useUTC=e._useUTC),t._f=e._f[i],Ot(t),g(t)&&(a=!0),r+=y(t).charsLeftOver,r+=10*y(t).unusedTokens.length,y(t).score=r,o?r<s&&(s=r,n=t):(null==s||r<s||a)&&(s=r,n=t,a&&(o=!0));c(e,n||t)}(e):i?Ot(e):r(n=(t=e)._i)?t._d=new Date(f.now()):a(n)?t._d=new Date(n.valueOf()):"string"==typeof n?function(e){var t=wt.exec(e._i);null===t?(kt(e),!1===e._isValid&&(delete e._isValid,Dt(e),!1===e._isValid&&(delete e._isValid,e._strict?e._isValid=!1:f.createFromInputFallback(e)))):e._d=new Date(+t[1])}(t):o(n)?(t._a=d(n.slice(0),function(e){return parseInt(e,10)}),Yt(t)):u(n)?function(e){var t,n;e._d||(n=void 0===(t=G(e._i)).day?t.date:t.day,e._a=d([t.year,t.month,n,t.hour,t.minute,t.second,t.millisecond],function(e){return e&&parseInt(e,10)}),Yt(e))}(t):h(n)?t._d=new Date(n):f.createFromInputFallback(t),g(e)||(e._d=null),e))}function xt(e,t,n,s,i){var r,a={};return!0!==t&&!1!==t||(s=t,t=void 0),!0!==n&&!1!==n||(s=n,n=void 0),(u(e)&&l(e)||o(e)&&0===e.length)&&(e=void 0),a._isAMomentObject=!0,a._useUTC=a._isUTC=i,a._l=n,a._i=e,a._f=t,a._strict=s,(r=new k(ct(bt(a))))._nextDay&&(r.add(1,"d"),r._nextDay=void 0),r}function Tt(e,t,n,s){return xt(e,t,n,s,!1)}f.createFromInputFallback=n("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(e){e._d=new Date(e._i+(e._useUTC?" UTC":""))}),f.ISO_8601=function(){},f.RFC_2822=function(){};var Nt=n("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=Tt.apply(null,arguments);return this.isValid()&&e.isValid()?e<this?this:e:w()}),Pt=n("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=Tt.apply(null,arguments);return this.isValid()&&e.isValid()?this<e?this:e:w()});function Rt(e,t){var n,s;if(1===t.length&&o(t[0])&&(t=t[0]),!t.length)return Tt();for(n=t[0],s=1;s<t.length;++s)t[s].isValid()&&!t[s][e](n)||(n=t[s]);return n}var Wt=["year","quarter","month","week","day","hour","minute","second","millisecond"];function Ct(e){var t=G(e),n=t.year||0,s=t.quarter||0,i=t.month||0,r=t.week||t.isoWeek||0,a=t.day||0,o=t.hour||0,u=t.minute||0,l=t.second||0,h=t.millisecond||0;this._isValid=function(e){var t,n,s=!1;for(t in e)if(m(e,t)&&(-1===we.call(Wt,t)||null!=e[t]&&isNaN(e[t])))return!1;for(n=0;n<Wt.length;++n)if(e[Wt[n]]){if(s)return!1;parseFloat(e[Wt[n]])!==Z(e[Wt[n]])&&(s=!0)}return!0}(t),this._milliseconds=+h+1e3*l+6e4*u+1e3*o*60*60,this._days=+a+7*r,this._months=+i+3*s+12*n,this._data={},this._locale=dt(),this._bubble()}function Ut(e){return e instanceof Ct}function Ht(e){return e<0?-1*Math.round(-1*e):Math.round(e)}function Ft(e,n){C(e,0,0,function(){var e=this.utcOffset(),t="+";return e<0&&(e=-e,t="-"),t+T(~~(e/60),2)+n+T(~~e%60,2)})}Ft("Z",":"),Ft("ZZ",""),ce("Z",he),ce("ZZ",he),ye(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Vt(he,e)});var Lt=/([\+\-]|\d\d)/gi;function Vt(e,t){var n,s,i=(t||"").match(e);return null===i?null:0===(s=60*(n=((i[i.length-1]||[])+"").match(Lt)||["-",0,0])[1]+Z(n[2]))?0:"+"===n[0]?s:-s}function Gt(e,t){var n,s;return t._isUTC?(n=t.clone(),s=(M(e)||a(e)?e.valueOf():Tt(e).valueOf())-n.valueOf(),n._d.setTime(n._d.valueOf()+s),f.updateOffset(n,!1),n):Tt(e).local()}function Et(e){return-Math.round(e._d.getTimezoneOffset())}function At(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}f.updateOffset=function(){};var jt=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,It=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Zt(e,t){var n,s,i,r=e,a=null;return Ut(e)?r={ms:e._milliseconds,d:e._days,M:e._months}:h(e)||!isNaN(+e)?(r={},t?r[t]=+e:r.milliseconds=+e):(a=jt.exec(e))?(n="-"===a[1]?-1:1,r={y:0,d:Z(a[ke])*n,h:Z(a[Me])*n,m:Z(a[De])*n,s:Z(a[Se])*n,ms:Z(Ht(1e3*a[Ye]))*n}):(a=It.exec(e))?(n="-"===a[1]?-1:1,r={y:zt(a[2],n),M:zt(a[3],n),w:zt(a[4],n),d:zt(a[5],n),h:zt(a[6],n),m:zt(a[7],n),s:zt(a[8],n)}):null==r?r={}:"object"==typeof r&&("from"in r||"to"in r)&&(i=function(e,t){var n;if(!e.isValid()||!t.isValid())return{milliseconds:0,months:0};t=Gt(t,e),e.isBefore(t)?n=$t(e,t):((n=$t(t,e)).milliseconds=-n.milliseconds,n.months=-n.months);return n}(Tt(r.from),Tt(r.to)),(r={}).ms=i.milliseconds,r.M=i.months),s=new Ct(r),Ut(e)&&m(e,"_locale")&&(s._locale=e._locale),Ut(e)&&m(e,"_isValid")&&(s._isValid=e._isValid),s}function zt(e,t){var n=e&&parseFloat(e.replace(",","."));return(isNaN(n)?0:n)*t}function $t(e,t){var n={};return n.months=t.month()-e.month()+12*(t.year()-e.year()),e.clone().add(n.months,"M").isAfter(t)&&--n.months,n.milliseconds=t-e.clone().add(n.months,"M"),n}function qt(s,i){return function(e,t){var n;return null===t||isNaN(+t)||(Y(i,"moment()."+i+"(period, number) is deprecated. Please use moment()."+i+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),n=e,e=t,t=n),Bt(this,Zt(e,t),s),this}}function Bt(e,t,n,s){var i=t._milliseconds,r=Ht(t._days),a=Ht(t._months);e.isValid()&&(s=null==s||s,a&&Ce(e,$(e,"Month")+a*n),r&&q(e,"Date",$(e,"Date")+r*n),i&&e._d.setTime(e._d.valueOf()+i*n),s&&f.updateOffset(e,r||a))}Zt.fn=Ct.prototype,Zt.invalid=function(){return Zt(NaN)};var Jt=qt(1,"add"),Qt=qt(-1,"subtract");function Xt(e){return"string"==typeof e||e instanceof String}function Kt(e){return M(e)||a(e)||Xt(e)||h(e)||function(t){var e=o(t),n=!1;e&&(n=0===t.filter(function(e){return!h(e)&&Xt(t)}).length);return e&&n}(e)||function(e){var t,n,s=u(e)&&!l(e),i=!1,r=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"];for(t=0;t<r.length;t+=1)n=r[t],i=i||m(e,n);return s&&i}(e)||null==e}function en(e,t){if(e.date()<t.date())return-en(t,e);var n=12*(t.year()-e.year())+(t.month()-e.month()),s=e.clone().add(n,"months"),i=t-s<0?(t-s)/(s-e.clone().add(n-1,"months")):(t-s)/(e.clone().add(1+n,"months")-s);return-(n+i)||0}function tn(e){var t;return void 0===e?this._locale._abbr:(null!=(t=dt(e))&&(this._locale=t),this)}f.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",f.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var nn=n("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});function sn(){return this._locale}var rn=126227808e5;function an(e,t){return(e%t+t)%t}function on(e,t,n){return e<100&&0<=e?new Date(e+400,t,n)-rn:new Date(e,t,n).valueOf()}function un(e,t,n){return e<100&&0<=e?Date.UTC(e+400,t,n)-rn:Date.UTC(e,t,n)}function ln(e,t){return t.erasAbbrRegex(e)}function hn(){for(var e=[],t=[],n=[],s=[],i=this.eras(),r=0,a=i.length;r<a;++r)t.push(me(i[r].name)),e.push(me(i[r].abbr)),n.push(me(i[r].narrow)),s.push(me(i[r].name)),s.push(me(i[r].abbr)),s.push(me(i[r].narrow));this._erasRegex=new RegExp("^("+s.join("|")+")","i"),this._erasNameRegex=new RegExp("^("+t.join("|")+")","i"),this._erasAbbrRegex=new RegExp("^("+e.join("|")+")","i"),this._erasNarrowRegex=new RegExp("^("+n.join("|")+")","i")}function dn(e,t){C(0,[e,e.length],0,t)}function cn(e,t,n,s,i){var r;return null==e?Ae(this,s,i).year:((r=je(e,s,i))<t&&(t=r),function(e,t,n,s,i){var r=Ee(e,t,n,s,i),a=Ve(r.year,0,r.dayOfYear);return this.year(a.getUTCFullYear()),this.month(a.getUTCMonth()),this.date(a.getUTCDate()),this}.call(this,e,t,n,s,i))}C("N",0,0,"eraAbbr"),C("NN",0,0,"eraAbbr"),C("NNN",0,0,"eraAbbr"),C("NNNN",0,0,"eraName"),C("NNNNN",0,0,"eraNarrow"),C("y",["y",1],"yo","eraYear"),C("y",["yy",2],0,"eraYear"),C("y",["yyy",3],0,"eraYear"),C("y",["yyyy",4],0,"eraYear"),ce("N",ln),ce("NN",ln),ce("NNN",ln),ce("NNNN",function(e,t){return t.erasNameRegex(e)}),ce("NNNNN",function(e,t){return t.erasNarrowRegex(e)}),ye(["N","NN","NNN","NNNN","NNNNN"],function(e,t,n,s){var i=n._locale.erasParse(e,s,n._strict);i?y(n).era=i:y(n).invalidEra=e}),ce("y",oe),ce("yy",oe),ce("yyy",oe),ce("yyyy",oe),ce("yo",function(e,t){return t._eraYearOrdinalRegex||oe}),ye(["y","yy","yyy","yyyy"],pe),ye(["yo"],function(e,t,n,s){var i;n._locale._eraYearOrdinalRegex&&(i=e.match(n._locale._eraYearOrdinalRegex)),n._locale.eraYearOrdinalParse?t[pe]=n._locale.eraYearOrdinalParse(e,i):t[pe]=parseInt(e,10)}),C(0,["gg",2],0,function(){return this.weekYear()%100}),C(0,["GG",2],0,function(){return this.isoWeekYear()%100}),dn("gggg","weekYear"),dn("ggggg","weekYear"),dn("GGGG","isoWeekYear"),dn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),A("weekYear",1),A("isoWeekYear",1),ce("G",ue),ce("g",ue),ce("GG",te,Q),ce("gg",te,Q),ce("GGGG",re,K),ce("gggg",re,K),ce("GGGGG",ae,ee),ce("ggggg",ae,ee),ge(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,s){t[s.substr(0,2)]=Z(e)}),ge(["gg","GG"],function(e,t,n,s){t[s]=f.parseTwoDigitYear(e)}),C("Q",0,"Qo","quarter"),L("quarter","Q"),A("quarter",7),ce("Q",J),ye("Q",function(e,t){t[ve]=3*(Z(e)-1)}),C("D",["DD",2],"Do","date"),L("date","D"),A("date",9),ce("D",te),ce("DD",te,Q),ce("Do",function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient}),ye(["D","DD"],ke),ye("Do",function(e,t){t[ke]=Z(e.match(te)[0])});var fn=z("Date",!0);C("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),A("dayOfYear",4),ce("DDD",ie),ce("DDDD",X),ye(["DDD","DDDD"],function(e,t,n){n._dayOfYear=Z(e)}),C("m",["mm",2],0,"minute"),L("minute","m"),A("minute",14),ce("m",te),ce("mm",te,Q),ye(["m","mm"],De);var mn=z("Minutes",!1);C("s",["ss",2],0,"second"),L("second","s"),A("second",15),ce("s",te),ce("ss",te,Q),ye(["s","ss"],Se);var _n,yn,gn=z("Seconds",!1);for(C("S",0,0,function(){return~~(this.millisecond()/100)}),C(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),C(0,["SSS",3],0,"millisecond"),C(0,["SSSS",4],0,function(){return 10*this.millisecond()}),C(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),C(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),C(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),C(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),C(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),L("millisecond","ms"),A("millisecond",16),ce("S",ie,J),ce("SS",ie,Q),ce("SSS",ie,X),_n="SSSS";_n.length<=9;_n+="S")ce(_n,oe);function wn(e,t){t[Ye]=Z(1e3*("0."+e))}for(_n="S";_n.length<=9;_n+="S")ye(_n,wn);yn=z("Milliseconds",!1),C("z",0,0,"zoneAbbr"),C("zz",0,0,"zoneName");var pn=k.prototype;function vn(e){return e}pn.add=Jt,pn.calendar=function(e,t){1===arguments.length&&(arguments[0]?Kt(arguments[0])?(e=arguments[0],t=void 0):function(e){for(var t=u(e)&&!l(e),n=!1,s=["sameDay","nextDay","lastDay","nextWeek","lastWeek","sameElse"],i=0;i<s.length;i+=1)n=n||m(e,s[i]);return t&&n}(arguments[0])&&(t=arguments[0],e=void 0):t=e=void 0);var n=e||Tt(),s=Gt(n,this).startOf("day"),i=f.calendarFormat(this,s)||"sameElse",r=t&&(O(t[i])?t[i].call(this,n):t[i]);return this.format(r||this.localeData().calendar(i,this,Tt(n)))},pn.clone=function(){return new k(this)},pn.diff=function(e,t,n){var s,i,r;if(!this.isValid())return NaN;if(!(s=Gt(e,this)).isValid())return NaN;switch(i=6e4*(s.utcOffset()-this.utcOffset()),t=V(t)){case"year":r=en(this,s)/12;break;case"month":r=en(this,s);break;case"quarter":r=en(this,s)/3;break;case"second":r=(this-s)/1e3;break;case"minute":r=(this-s)/6e4;break;case"hour":r=(this-s)/36e5;break;case"day":r=(this-s-i)/864e5;break;case"week":r=(this-s-i)/6048e5;break;default:r=this-s}return n?r:I(r)},pn.endOf=function(e){var t,n;if(void 0===(e=V(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?un:on,e){case"year":t=n(this.year()+1,0,1)-1;break;case"quarter":t=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":t=n(this.year(),this.month()+1,1)-1;break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":t=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":t=this._d.valueOf(),t+=36e5-an(t+(this._isUTC?0:6e4*this.utcOffset()),36e5)-1;break;case"minute":t=this._d.valueOf(),t+=6e4-an(t,6e4)-1;break;case"second":t=this._d.valueOf(),t+=1e3-an(t,1e3)-1;break}return this._d.setTime(t),f.updateOffset(this,!0),this},pn.format=function(e){e=e||(this.isUtc()?f.defaultFormatUtc:f.defaultFormat);var t=U(this,e);return this.localeData().postformat(t)},pn.from=function(e,t){return this.isValid()&&(M(e)&&e.isValid()||Tt(e).isValid())?Zt({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},pn.fromNow=function(e){return this.from(Tt(),e)},pn.to=function(e,t){return this.isValid()&&(M(e)&&e.isValid()||Tt(e).isValid())?Zt({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},pn.toNow=function(e){return this.to(Tt(),e)},pn.get=function(e){return O(this[e=V(e)])?this[e]():this},pn.invalidAt=function(){return y(this).overflow},pn.isAfter=function(e,t){var n=M(e)?e:Tt(e);return!(!this.isValid()||!n.isValid())&&("millisecond"===(t=V(t)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()<this.clone().startOf(t).valueOf())},pn.isBefore=function(e,t){var n=M(e)?e:Tt(e);return!(!this.isValid()||!n.isValid())&&("millisecond"===(t=V(t)||"millisecond")?this.valueOf()<n.valueOf():this.clone().endOf(t).valueOf()<n.valueOf())},pn.isBetween=function(e,t,n,s){var i=M(e)?e:Tt(e),r=M(t)?t:Tt(t);return!!(this.isValid()&&i.isValid()&&r.isValid())&&(("("===(s=s||"()")[0]?this.isAfter(i,n):!this.isBefore(i,n))&&(")"===s[1]?this.isBefore(r,n):!this.isAfter(r,n)))},pn.isSame=function(e,t){var n,s=M(e)?e:Tt(e);return!(!this.isValid()||!s.isValid())&&("millisecond"===(t=V(t)||"millisecond")?this.valueOf()===s.valueOf():(n=s.valueOf(),this.clone().startOf(t).valueOf()<=n&&n<=this.clone().endOf(t).valueOf()))},pn.isSameOrAfter=function(e,t){return this.isSame(e,t)||this.isAfter(e,t)},pn.isSameOrBefore=function(e,t){return this.isSame(e,t)||this.isBefore(e,t)},pn.isValid=function(){return g(this)},pn.lang=nn,pn.locale=tn,pn.localeData=sn,pn.max=Pt,pn.min=Nt,pn.parsingFlags=function(){return c({},y(this))},pn.set=function(e,t){if("object"==typeof e)for(var n=function(e){var t,n=[];for(t in e)m(e,t)&&n.push({unit:t,priority:E[t]});return n.sort(function(e,t){return e.priority-t.priority}),n}(e=G(e)),s=0;s<n.length;s++)this[n[s].unit](e[n[s].unit]);else if(O(this[e=V(e)]))return this[e](t);return this},pn.startOf=function(e){var t,n;if(void 0===(e=V(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?un:on,e){case"year":t=n(this.year(),0,1);break;case"quarter":t=n(this.year(),this.month()-this.month()%3,1);break;case"month":t=n(this.year(),this.month(),1);break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":t=n(this.year(),this.month(),this.date());break;case"hour":t=this._d.valueOf(),t-=an(t+(this._isUTC?0:6e4*this.utcOffset()),36e5);break;case"minute":t=this._d.valueOf(),t-=an(t,6e4);break;case"second":t=this._d.valueOf(),t-=an(t,1e3);break}return this._d.setTime(t),f.updateOffset(this,!0),this},pn.subtract=Qt,pn.toArray=function(){var e=this;return[e.year(),e.month(),e.date(),e.hour(),e.minute(),e.second(),e.millisecond()]},pn.toObject=function(){var e=this;return{years:e.year(),months:e.month(),date:e.date(),hours:e.hours(),minutes:e.minutes(),seconds:e.seconds(),milliseconds:e.milliseconds()}},pn.toDate=function(){return new Date(this.valueOf())},pn.toISOString=function(e){if(!this.isValid())return null;var t=!0!==e,n=t?this.clone().utc():this;return n.year()<0||9999<n.year()?U(n,t?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?t?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,t?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},pn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e,t,n,s="moment",i="";return this.isLocal()||(s=0===this.utcOffset()?"moment.utc":"moment.parseZone",i="Z"),e="["+s+'("]',t=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",n=i+'[")]',this.format(e+t+"-MM-DD[T]HH:mm:ss.SSS"+n)},"undefined"!=typeof Symbol&&null!=Symbol.for&&(pn[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),pn.toJSON=function(){return this.isValid()?this.toISOString():null},pn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},pn.unix=function(){return Math.floor(this.valueOf()/1e3)},pn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},pn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},pn.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].name;if(t[n].until<=e&&e<=t[n].since)return t[n].name}return""},pn.eraNarrow=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].narrow;if(t[n].until<=e&&e<=t[n].since)return t[n].narrow}return""},pn.eraAbbr=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].abbr;if(t[n].until<=e&&e<=t[n].since)return t[n].abbr}return""},pn.eraYear=function(){for(var e,t,n=this.localeData().eras(),s=0,i=n.length;s<i;++s)if(e=n[s].since<=n[s].until?1:-1,t=this.clone().startOf("day").valueOf(),n[s].since<=t&&t<=n[s].until||n[s].until<=t&&t<=n[s].since)return(this.year()-f(n[s].since).year())*e+n[s].offset;return this.year()},pn.year=Le,pn.isLeapYear=function(){return j(this.year())},pn.weekYear=function(e){return cn.call(this,e,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},pn.isoWeekYear=function(e){return cn.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},pn.quarter=pn.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},pn.month=Ue,pn.daysInMonth=function(){return xe(this.year(),this.month())},pn.week=pn.weeks=function(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")},pn.isoWeek=pn.isoWeeks=function(e){var t=Ae(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")},pn.weeksInYear=function(){var e=this.localeData()._week;return je(this.year(),e.dow,e.doy)},pn.weeksInWeekYear=function(){var e=this.localeData()._week;return je(this.weekYear(),e.dow,e.doy)},pn.isoWeeksInYear=function(){return je(this.year(),1,4)},pn.isoWeeksInISOWeekYear=function(){return je(this.isoWeekYear(),1,4)},pn.date=fn,pn.day=pn.days=function(e){if(!this.isValid())return null!=e?this:NaN;var t,n,s=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(t=e,n=this.localeData(),e="string"!=typeof t?t:isNaN(t)?"number"==typeof(t=n.weekdaysParse(t))?t:null:parseInt(t,10),this.add(e-s,"d")):s},pn.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")},pn.isoWeekday=function(e){if(!this.isValid())return null!=e?this:NaN;if(null==e)return this.day()||7;var t,n,s=(t=e,n=this.localeData(),"string"==typeof t?n.weekdaysParse(t)%7||7:isNaN(t)?null:t);return this.day(this.day()%7?s:s-7)},pn.dayOfYear=function(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")},pn.hour=pn.hours=tt,pn.minute=pn.minutes=mn,pn.second=pn.seconds=gn,pn.millisecond=pn.milliseconds=yn,pn.utcOffset=function(e,t,n){var s,i=this._offset||0;if(!this.isValid())return null!=e?this:NaN;if(null==e)return this._isUTC?i:Et(this);if("string"==typeof e){if(null===(e=Vt(he,e)))return this}else Math.abs(e)<16&&!n&&(e*=60);return!this._isUTC&&t&&(s=Et(this)),this._offset=e,this._isUTC=!0,null!=s&&this.add(s,"m"),i!==e&&(!t||this._changeInProgress?Bt(this,Zt(e-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,f.updateOffset(this,!0),this._changeInProgress=null)),this},pn.utc=function(e){return this.utcOffset(0,e)},pn.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(Et(this),"m")),this},pn.parseZone=function(){var e;return null!=this._tzm?this.utcOffset(this._tzm,!1,!0):"string"==typeof this._i&&(null!=(e=Vt(le,this._i))?this.utcOffset(e):this.utcOffset(0,!0)),this},pn.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?Tt(e).utcOffset():0,(this.utcOffset()-e)%60==0)},pn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},pn.isLocal=function(){return!!this.isValid()&&!this._isUTC},pn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},pn.isUtc=At,pn.isUTC=At,pn.zoneAbbr=function(){return this._isUTC?"UTC":""},pn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},pn.dates=n("dates accessor is deprecated. Use date instead.",fn),pn.months=n("months accessor is deprecated. Use month instead",Ue),pn.years=n("years accessor is deprecated. Use year instead",Le),pn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),pn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!r(this._isDSTShifted))return this._isDSTShifted;var e,t={};return v(t,this),(t=bt(t))._a?(e=(t._isUTC?_:Tt)(t._a),this._isDSTShifted=this.isValid()&&0<function(e,t,n){for(var s=Math.min(e.length,t.length),i=Math.abs(e.length-t.length),r=0,a=0;a<s;a++)(n&&e[a]!==t[a]||!n&&Z(e[a])!==Z(t[a]))&&r++;return r+i}(t._a,e.toArray())):this._isDSTShifted=!1,this._isDSTShifted});var kn=x.prototype;function Mn(e,t,n,s){var i=dt(),r=_().set(s,t);return i[n](r,e)}function Dn(e,t,n){if(h(e)&&(t=e,e=void 0),e=e||"",null!=t)return Mn(e,t,n,"month");for(var s=[],i=0;i<12;i++)s[i]=Mn(e,i,n,"month");return s}function Sn(e,t,n,s){t=("boolean"==typeof e?h(t)&&(n=t,t=void 0):(t=e,e=!1,h(n=t)&&(n=t,t=void 0)),t||"");var i,r=dt(),a=e?r._week.dow:0,o=[];if(null!=n)return Mn(t,(n+a)%7,s,"day");for(i=0;i<7;i++)o[i]=Mn(t,(i+a)%7,s,"day");return o}kn.calendar=function(e,t,n){var s=this._calendar[e]||this._calendar.sameElse;return O(s)?s.call(t,n):s},kn.longDateFormat=function(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.match(N).map(function(e){return"MMMM"===e||"MM"===e||"DD"===e||"dddd"===e?e.slice(1):e}).join(""),this._longDateFormat[e])},kn.invalidDate=function(){return this._invalidDate},kn.ordinal=function(e){return this._ordinal.replace("%d",e)},kn.preparse=vn,kn.postformat=vn,kn.relativeTime=function(e,t,n,s){var i=this._relativeTime[n];return O(i)?i(e,t,n,s):i.replace(/%d/i,e)},kn.pastFuture=function(e,t){var n=this._relativeTime[0<e?"future":"past"];return O(n)?n(t):n.replace(/%s/i,t)},kn.set=function(e){var t,n;for(n in e)m(e,n)&&(O(t=e[n])?this[n]=t:this["_"+n]=t);this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},kn.eras=function(e,t){for(var n,s=this._eras||dt("en")._eras,i=0,r=s.length;i<r;++i){switch(typeof s[i].since){case"string":n=f(s[i].since).startOf("day"),s[i].since=n.valueOf();break}switch(typeof s[i].until){case"undefined":s[i].until=1/0;break;case"string":n=f(s[i].until).startOf("day").valueOf(),s[i].until=n.valueOf();break}}return s},kn.erasParse=function(e,t,n){var s,i,r,a,o,u=this.eras();for(e=e.toUpperCase(),s=0,i=u.length;s<i;++s)if(r=u[s].name.toUpperCase(),a=u[s].abbr.toUpperCase(),o=u[s].narrow.toUpperCase(),n)switch(t){case"N":case"NN":case"NNN":if(a===e)return u[s];break;case"NNNN":if(r===e)return u[s];break;case"NNNNN":if(o===e)return u[s];break}else if(0<=[r,a,o].indexOf(e))return u[s]},kn.erasConvertYear=function(e,t){var n=e.since<=e.until?1:-1;return void 0===t?f(e.since).year():f(e.since).year()+(t-e.offset)*n},kn.erasAbbrRegex=function(e){return m(this,"_erasAbbrRegex")||hn.call(this),e?this._erasAbbrRegex:this._erasRegex},kn.erasNameRegex=function(e){return m(this,"_erasNameRegex")||hn.call(this),e?this._erasNameRegex:this._erasRegex},kn.erasNarrowRegex=function(e){return m(this,"_erasNarrowRegex")||hn.call(this),e?this._erasNarrowRegex:this._erasRegex},kn.months=function(e,t){return e?o(this._months)?this._months[e.month()]:this._months[(this._months.isFormat||Pe).test(t)?"format":"standalone"][e.month()]:o(this._months)?this._months:this._months.standalone},kn.monthsShort=function(e,t){return e?o(this._monthsShort)?this._monthsShort[e.month()]:this._monthsShort[Pe.test(t)?"format":"standalone"][e.month()]:o(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},kn.monthsParse=function(e,t,n){var s,i,r;if(this._monthsParseExact)return function(e,t,n){var s,i,r,a=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],s=0;s<12;++s)r=_([2e3,s]),this._shortMonthsParse[s]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[s]=this.months(r,"").toLocaleLowerCase();return n?"MMM"===t?-1!==(i=we.call(this._shortMonthsParse,a))?i:null:-1!==(i=we.call(this._longMonthsParse,a))?i:null:"MMM"===t?-1!==(i=we.call(this._shortMonthsParse,a))||-1!==(i=we.call(this._longMonthsParse,a))?i:null:-1!==(i=we.call(this._longMonthsParse,a))||-1!==(i=we.call(this._shortMonthsParse,a))?i:null}.call(this,e,t,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),s=0;s<12;s++){if(i=_([2e3,s]),n&&!this._longMonthsParse[s]&&(this._longMonthsParse[s]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[s]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[s]||(r="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[s]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===t&&this._longMonthsParse[s].test(e))return s;if(n&&"MMM"===t&&this._shortMonthsParse[s].test(e))return s;if(!n&&this._monthsParse[s].test(e))return s}},kn.monthsRegex=function(e){return this._monthsParseExact?(m(this,"_monthsRegex")||He.call(this),e?this._monthsStrictRegex:this._monthsRegex):(m(this,"_monthsRegex")||(this._monthsRegex=We),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},kn.monthsShortRegex=function(e){return this._monthsParseExact?(m(this,"_monthsRegex")||He.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(m(this,"_monthsShortRegex")||(this._monthsShortRegex=Re),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},kn.week=function(e){return Ae(e,this._week.dow,this._week.doy).week},kn.firstDayOfYear=function(){return this._week.doy},kn.firstDayOfWeek=function(){return this._week.dow},kn.weekdays=function(e,t){var n=o(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?"format":"standalone"];return!0===e?Ie(n,this._week.dow):e?n[e.day()]:n},kn.weekdaysMin=function(e){return!0===e?Ie(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin},kn.weekdaysShort=function(e){return!0===e?Ie(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort},kn.weekdaysParse=function(e,t,n){var s,i,r;if(this._weekdaysParseExact)return function(e,t,n){var s,i,r,a=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],s=0;s<7;++s)r=_([2e3,1]).day(s),this._minWeekdaysParse[s]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[s]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[s]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===t?-1!==(i=we.call(this._weekdaysParse,a))?i:null:"ddd"===t?-1!==(i=we.call(this._shortWeekdaysParse,a))?i:null:-1!==(i=we.call(this._minWeekdaysParse,a))?i:null:"dddd"===t?-1!==(i=we.call(this._weekdaysParse,a))||-1!==(i=we.call(this._shortWeekdaysParse,a))||-1!==(i=we.call(this._minWeekdaysParse,a))?i:null:"ddd"===t?-1!==(i=we.call(this._shortWeekdaysParse,a))||-1!==(i=we.call(this._weekdaysParse,a))||-1!==(i=we.call(this._minWeekdaysParse,a))?i:null:-1!==(i=we.call(this._minWeekdaysParse,a))||-1!==(i=we.call(this._weekdaysParse,a))||-1!==(i=we.call(this._shortWeekdaysParse,a))?i:null}.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),s=0;s<7;s++){if(i=_([2e3,1]).day(s),n&&!this._fullWeekdaysParse[s]&&(this._fullWeekdaysParse[s]=new RegExp("^"+this.weekdays(i,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[s]=new RegExp("^"+this.weekdaysShort(i,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[s]=new RegExp("^"+this.weekdaysMin(i,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[s]||(r="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[s]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[s].test(e))return s;if(n&&"ddd"===t&&this._shortWeekdaysParse[s].test(e))return s;if(n&&"dd"===t&&this._minWeekdaysParse[s].test(e))return s;if(!n&&this._weekdaysParse[s].test(e))return s}},kn.weekdaysRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Qe.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(m(this,"_weekdaysRegex")||(this._weekdaysRegex=qe),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},kn.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Qe.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(m(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Be),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},kn.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Qe.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(m(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Je),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},kn.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},kn.meridiem=function(e,t,n){return 11<e?n?"pm":"PM":n?"am":"AM"},lt("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===Z(e%100/10)?"th":1==t?"st":2==t?"nd":3==t?"rd":"th")}}),f.lang=n("moment.lang is deprecated. Use moment.locale instead.",lt),f.langData=n("moment.langData is deprecated. Use moment.localeData instead.",dt);var Yn=Math.abs;function On(e,t,n,s){var i=Zt(t,n);return e._milliseconds+=s*i._milliseconds,e._days+=s*i._days,e._months+=s*i._months,e._bubble()}function bn(e){return e<0?Math.floor(e):Math.ceil(e)}function xn(e){return 4800*e/146097}function Tn(e){return 146097*e/4800}function Nn(e){return function(){return this.as(e)}}var Pn=Nn("ms"),Rn=Nn("s"),Wn=Nn("m"),Cn=Nn("h"),Un=Nn("d"),Hn=Nn("w"),Fn=Nn("M"),Ln=Nn("Q"),Vn=Nn("y");function Gn(e){return function(){return this.isValid()?this._data[e]:NaN}}var En=Gn("milliseconds"),An=Gn("seconds"),jn=Gn("minutes"),In=Gn("hours"),Zn=Gn("days"),zn=Gn("months"),$n=Gn("years");var qn=Math.round,Bn={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function Jn(e,t,n,s){var i=Zt(e).abs(),r=qn(i.as("s")),a=qn(i.as("m")),o=qn(i.as("h")),u=qn(i.as("d")),l=qn(i.as("M")),h=qn(i.as("w")),d=qn(i.as("y")),c=(r<=n.ss?["s",r]:r<n.s&&["ss",r])||a<=1&&["m"]||a<n.m&&["mm",a]||o<=1&&["h"]||o<n.h&&["hh",o]||u<=1&&["d"]||u<n.d&&["dd",u];return null!=n.w&&(c=c||h<=1&&["w"]||h<n.w&&["ww",h]),(c=c||l<=1&&["M"]||l<n.M&&["MM",l]||d<=1&&["y"]||["yy",d])[2]=t,c[3]=0<+e,c[4]=s,function(e,t,n,s,i){return i.relativeTime(t||1,!!n,e,s)}.apply(null,c)}var Qn=Math.abs;function Xn(e){return(0<e)-(e<0)||+e}function Kn(){if(!this.isValid())return this.localeData().invalidDate();var e,t,n,s,i,r,a,o,u=Qn(this._milliseconds)/1e3,l=Qn(this._days),h=Qn(this._months),d=this.asSeconds();return d?(e=I(u/60),t=I(e/60),u%=60,e%=60,n=I(h/12),h%=12,s=u?u.toFixed(3).replace(/\.?0+$/,""):"",i=d<0?"-":"",r=Xn(this._months)!==Xn(d)?"-":"",a=Xn(this._days)!==Xn(d)?"-":"",o=Xn(this._milliseconds)!==Xn(d)?"-":"",i+"P"+(n?r+n+"Y":"")+(h?r+h+"M":"")+(l?a+l+"D":"")+(t||e||u?"T":"")+(t?o+t+"H":"")+(e?o+e+"M":"")+(u?o+s+"S":"")):"P0D"}var es=Ct.prototype;return es.isValid=function(){return this._isValid},es.abs=function(){var e=this._data;return this._milliseconds=Yn(this._milliseconds),this._days=Yn(this._days),this._months=Yn(this._months),e.milliseconds=Yn(e.milliseconds),e.seconds=Yn(e.seconds),e.minutes=Yn(e.minutes),e.hours=Yn(e.hours),e.months=Yn(e.months),e.years=Yn(e.years),this},es.add=function(e,t){return On(this,e,t,1)},es.subtract=function(e,t){return On(this,e,t,-1)},es.as=function(e){if(!this.isValid())return NaN;var t,n,s=this._milliseconds;if("month"===(e=V(e))||"quarter"===e||"year"===e)switch(t=this._days+s/864e5,n=this._months+xn(t),e){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(t=this._days+Math.round(Tn(this._months)),e){case"week":return t/7+s/6048e5;case"day":return t+s/864e5;case"hour":return 24*t+s/36e5;case"minute":return 1440*t+s/6e4;case"second":return 86400*t+s/1e3;case"millisecond":return Math.floor(864e5*t)+s;default:throw new Error("Unknown unit "+e)}},es.asMilliseconds=Pn,es.asSeconds=Rn,es.asMinutes=Wn,es.asHours=Cn,es.asDays=Un,es.asWeeks=Hn,es.asMonths=Fn,es.asQuarters=Ln,es.asYears=Vn,es.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*Z(this._months/12):NaN},es._bubble=function(){var e,t,n,s,i,r=this._milliseconds,a=this._days,o=this._months,u=this._data;return 0<=r&&0<=a&&0<=o||r<=0&&a<=0&&o<=0||(r+=864e5*bn(Tn(o)+a),o=a=0),u.milliseconds=r%1e3,e=I(r/1e3),u.seconds=e%60,t=I(e/60),u.minutes=t%60,n=I(t/60),u.hours=n%24,a+=I(n/24),o+=i=I(xn(a)),a-=bn(Tn(i)),s=I(o/12),o%=12,u.days=a,u.months=o,u.years=s,this},es.clone=function(){return Zt(this)},es.get=function(e){return e=V(e),this.isValid()?this[e+"s"]():NaN},es.milliseconds=En,es.seconds=An,es.minutes=jn,es.hours=In,es.days=Zn,es.weeks=function(){return I(this.days()/7)},es.months=zn,es.years=$n,es.humanize=function(e,t){if(!this.isValid())return this.localeData().invalidDate();var n,s,i=!1,r=Bn;return"object"==typeof e&&(t=e,e=!1),"boolean"==typeof e&&(i=e),"object"==typeof t&&(r=Object.assign({},Bn,t),null!=t.s&&null==t.ss&&(r.ss=t.s-1)),n=this.localeData(),s=Jn(this,!i,r,n),i&&(s=n.pastFuture(+this,s)),n.postformat(s)},es.toISOString=Kn,es.toString=Kn,es.toJSON=Kn,es.locale=tn,es.localeData=sn,es.toIsoString=n("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Kn),es.lang=nn,C("X",0,0,"unix"),C("x",0,0,"valueOf"),ce("x",ue),ce("X",/[+-]?\d+(\.\d{1,3})?/),ye("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e))}),ye("x",function(e,t,n){n._d=new Date(Z(e))}),f.version="2.29.1",e=Tt,f.fn=pn,f.min=function(){return Rt("isBefore",[].slice.call(arguments,0))},f.max=function(){return Rt("isAfter",[].slice.call(arguments,0))},f.now=function(){return Date.now?Date.now():+new Date},f.utc=_,f.unix=function(e){return Tt(1e3*e)},f.months=function(e,t){return Dn(e,t,"months")},f.isDate=a,f.locale=lt,f.invalid=w,f.duration=Zt,f.isMoment=M,f.weekdays=function(e,t,n){return Sn(e,t,n,"weekdays")},f.parseZone=function(){return Tt.apply(null,arguments).parseZone()},f.localeData=dt,f.isDuration=Ut,f.monthsShort=function(e,t){return Dn(e,t,"monthsShort")},f.weekdaysMin=function(e,t,n){return Sn(e,t,n,"weekdaysMin")},f.defineLocale=ht,f.updateLocale=function(e,t){var n,s,i;return null!=t?(i=st,null!=it[e]&&null!=it[e].parentLocale?it[e].set(b(it[e]._config,t)):(null!=(s=ut(e))&&(i=s._config),t=b(i,t),null==s&&(t.abbr=e),(n=new x(t)).parentLocale=it[e],it[e]=n),lt(e)):null!=it[e]&&(null!=it[e].parentLocale?(it[e]=it[e].parentLocale,e===lt()&<(e)):null!=it[e]&&delete it[e]),it[e]},f.locales=function(){return s(it)},f.weekdaysShort=function(e,t,n){return Sn(e,t,n,"weekdaysShort")},f.normalizeUnits=V,f.relativeTimeRounding=function(e){return void 0===e?qn:"function"==typeof e&&(qn=e,!0)},f.relativeTimeThreshold=function(e,t){return void 0!==Bn[e]&&(void 0===t?Bn[e]:(Bn[e]=t,"s"===e&&(Bn.ss=t-1),!0))},f.calendarFormat=function(e,t){var n=e.diff(t,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},f.prototype=pn,f.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},f}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var H;function f(){return H.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function F(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function c(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function L(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(c(e,t))return;return 1}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function V(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function G(e,t){for(var n=[],s=e.length,i=0;i<s;++i)n.push(t(e[i],i));return n}function E(e,t){for(var n in t)c(t,n)&&(e[n]=t[n]);return c(t,"toString")&&(e.toString=t.toString),c(t,"valueOf")&&(e.valueOf=t.valueOf),e}function l(e,t,n,s){return Pt(e,t,n,s,!0).utc()}function m(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function A(e){if(null==e._isValid){var t=m(e),n=j.call(t.parsedDateParts,function(e){return null!=e}),n=!isNaN(e._d.getTime())&&t.overflow<0&&!t.empty&&!t.invalidEra&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n);if(e._strict&&(n=n&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour),null!=Object.isFrozen&&Object.isFrozen(e))return n;e._isValid=n}return e._isValid}function I(e){var t=l(NaN);return null!=e?E(m(t),e):m(t).userInvalidated=!0,t}var j=Array.prototype.some||function(e){for(var t=Object(this),n=t.length>>>0,s=0;s<n;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1},Z=f.momentProperties=[],z=!1;function $(e,t){var n,s,i,r=Z.length;if(o(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),o(t._i)||(e._i=t._i),o(t._f)||(e._f=t._f),o(t._l)||(e._l=t._l),o(t._strict)||(e._strict=t._strict),o(t._tzm)||(e._tzm=t._tzm),o(t._isUTC)||(e._isUTC=t._isUTC),o(t._offset)||(e._offset=t._offset),o(t._pf)||(e._pf=m(t)),o(t._locale)||(e._locale=t._locale),0<r)for(n=0;n<r;n++)o(i=t[s=Z[n]])||(e[s]=i);return e}function q(e){$(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===z&&(z=!0,f.updateOffset(this),z=!1)}function h(e){return e instanceof q||null!=e&&null!=e._isAMomentObject}function B(e){!1===f.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function e(r,a){var o=!0;return E(function(){if(null!=f.deprecationHandler&&f.deprecationHandler(null,r),o){for(var e,t,n=[],s=arguments.length,i=0;i<s;i++){if(e="","object"==typeof arguments[i]){for(t in e+="\n["+i+"] ",arguments[0])c(arguments[0],t)&&(e+=t+": "+arguments[0][t]+", ");e=e.slice(0,-2)}else e=arguments[i];n.push(e)}B(r+"\nArguments: "+Array.prototype.slice.call(n).join("")+"\n"+(new Error).stack),o=!1}return a.apply(this,arguments)},a)}var J={};function Q(e,t){null!=f.deprecationHandler&&f.deprecationHandler(e,t),J[e]||(B(t),J[e]=!0)}function d(e){return"undefined"!=typeof Function&&e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function X(e,t){var n,s=E({},e);for(n in t)c(t,n)&&(F(e[n])&&F(t[n])?(s[n]={},E(s[n],e[n]),E(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)c(e,n)&&!c(t,n)&&F(e[n])&&(s[n]=E({},s[n]));return s}function K(e){null!=e&&this.set(e)}f.suppressDeprecationWarnings=!1,f.deprecationHandler=null;var ee=Object.keys||function(e){var t,n=[];for(t in e)c(e,t)&&n.push(t);return n};function r(e,t,n){var s=""+Math.abs(e);return(0<=e?n?"+":"":"-")+Math.pow(10,Math.max(0,t-s.length)).toString().substr(1)+s}var te=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,ne=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,se={},ie={};function s(e,t,n,s){var i="string"==typeof s?function(){return this[s]()}:s;e&&(ie[e]=i),t&&(ie[t[0]]=function(){return r(i.apply(this,arguments),t[1],t[2])}),n&&(ie[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function re(e,t){return e.isValid()?(t=ae(t,e.localeData()),se[t]=se[t]||function(s){for(var e,i=s.match(te),t=0,r=i.length;t<r;t++)ie[i[t]]?i[t]=ie[i[t]]:i[t]=(e=i[t]).match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"");return function(e){for(var t="",n=0;n<r;n++)t+=d(i[n])?i[n].call(e,s):i[n];return t}}(t),se[t](e)):e.localeData().invalidDate()}function ae(e,t){var n=5;function s(e){return t.longDateFormat(e)||e}for(ne.lastIndex=0;0<=n&&ne.test(e);)e=e.replace(ne,s),ne.lastIndex=0,--n;return e}var oe={};function t(e,t){var n=e.toLowerCase();oe[n]=oe[n+"s"]=oe[t]=e}function _(e){return"string"==typeof e?oe[e]||oe[e.toLowerCase()]:void 0}function ue(e){var t,n,s={};for(n in e)c(e,n)&&(t=_(n))&&(s[t]=e[n]);return s}var le={};function n(e,t){le[e]=t}function he(e){return e%4==0&&e%100!=0||e%400==0}function y(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function g(e){var e=+e,t=0;return t=0!=e&&isFinite(e)?y(e):t}function de(t,n){return function(e){return null!=e?(fe(this,t,e),f.updateOffset(this,n),this):ce(this,t)}}function ce(e,t){return e.isValid()?e._d["get"+(e._isUTC?"UTC":"")+t]():NaN}function fe(e,t,n){e.isValid()&&!isNaN(n)&&("FullYear"===t&&he(e.year())&&1===e.month()&&29===e.date()?(n=g(n),e._d["set"+(e._isUTC?"UTC":"")+t](n,e.month(),We(n,e.month()))):e._d["set"+(e._isUTC?"UTC":"")+t](n))}var i=/\d/,w=/\d\d/,me=/\d{3}/,_e=/\d{4}/,ye=/[+-]?\d{6}/,p=/\d\d?/,ge=/\d\d\d\d?/,we=/\d\d\d\d\d\d?/,pe=/\d{1,3}/,ke=/\d{1,4}/,ve=/[+-]?\d{1,6}/,Me=/\d+/,De=/[+-]?\d+/,Se=/Z|[+-]\d\d:?\d\d/gi,Ye=/Z|[+-]\d\d(?::?\d\d)?/gi,k=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i;function v(e,n,s){be[e]=d(n)?n:function(e,t){return e&&s?s:n}}function Oe(e,t){return c(be,e)?be[e](t._strict,t._locale):new RegExp(M(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,t,n,s,i){return t||n||s||i})))}function M(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var be={},xe={};function D(e,n){var t,s,i=n;for("string"==typeof e&&(e=[e]),u(n)&&(i=function(e,t){t[n]=g(e)}),s=e.length,t=0;t<s;t++)xe[e[t]]=i}function Te(e,i){D(e,function(e,t,n,s){n._w=n._w||{},i(e,n._w,n,s)})}var S,Y=0,O=1,b=2,x=3,T=4,N=5,Ne=6,Pe=7,Re=8;function We(e,t){if(isNaN(e)||isNaN(t))return NaN;var n=(t%(n=12)+n)%n;return e+=(t-n)/12,1==n?he(e)?29:28:31-n%7%2}S=Array.prototype.indexOf||function(e){for(var t=0;t<this.length;++t)if(this[t]===e)return t;return-1},s("M",["MM",2],"Mo",function(){return this.month()+1}),s("MMM",0,0,function(e){return this.localeData().monthsShort(this,e)}),s("MMMM",0,0,function(e){return this.localeData().months(this,e)}),t("month","M"),n("month",8),v("M",p),v("MM",p,w),v("MMM",function(e,t){return t.monthsShortRegex(e)}),v("MMMM",function(e,t){return t.monthsRegex(e)}),D(["M","MM"],function(e,t){t[O]=g(e)-1}),D(["MMM","MMMM"],function(e,t,n,s){s=n._locale.monthsParse(e,s,n._strict);null!=s?t[O]=s:m(n).invalidMonth=e});var Ce="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Ue="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),He=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Fe=k,Le=k;function Ve(e,t){var n;if(e.isValid()){if("string"==typeof t)if(/^\d+$/.test(t))t=g(t);else if(!u(t=e.localeData().monthsParse(t)))return;n=Math.min(e.date(),We(e.year(),t)),e._d["set"+(e._isUTC?"UTC":"")+"Month"](t,n)}}function Ge(e){return null!=e?(Ve(this,e),f.updateOffset(this,!0),this):ce(this,"Month")}function Ee(){function e(e,t){return t.length-e.length}for(var t,n=[],s=[],i=[],r=0;r<12;r++)t=l([2e3,r]),n.push(this.monthsShort(t,"")),s.push(this.months(t,"")),i.push(this.months(t,"")),i.push(this.monthsShort(t,""));for(n.sort(e),s.sort(e),i.sort(e),r=0;r<12;r++)n[r]=M(n[r]),s[r]=M(s[r]);for(r=0;r<24;r++)i[r]=M(i[r]);this._monthsRegex=new RegExp("^("+i.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+n.join("|")+")","i")}function Ae(e){return he(e)?366:365}s("Y",0,0,function(){var e=this.year();return e<=9999?r(e,4):"+"+e}),s(0,["YY",2],0,function(){return this.year()%100}),s(0,["YYYY",4],0,"year"),s(0,["YYYYY",5],0,"year"),s(0,["YYYYYY",6,!0],0,"year"),t("year","y"),n("year",1),v("Y",De),v("YY",p,w),v("YYYY",ke,_e),v("YYYYY",ve,ye),v("YYYYYY",ve,ye),D(["YYYYY","YYYYYY"],Y),D("YYYY",function(e,t){t[Y]=2===e.length?f.parseTwoDigitYear(e):g(e)}),D("YY",function(e,t){t[Y]=f.parseTwoDigitYear(e)}),D("Y",function(e,t){t[Y]=parseInt(e,10)}),f.parseTwoDigitYear=function(e){return g(e)+(68<g(e)?1900:2e3)};var Ie=de("FullYear",!0);function je(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}function Ze(e){var t;return e<100&&0<=e?((t=Array.prototype.slice.call(arguments))[0]=e+400,t=new Date(Date.UTC.apply(null,t)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)):t=new Date(Date.UTC.apply(null,arguments)),t}function ze(e,t,n){n=7+t-n;return n-(7+Ze(e,0,n).getUTCDay()-t)%7-1}function $e(e,t,n,s,i){var r,t=1+7*(t-1)+(7+n-s)%7+ze(e,s,i),n=t<=0?Ae(r=e-1)+t:t>Ae(e)?(r=e+1,t-Ae(e)):(r=e,t);return{year:r,dayOfYear:n}}function qe(e,t,n){var s,i,r=ze(e.year(),t,n),r=Math.floor((e.dayOfYear()-r-1)/7)+1;return r<1?s=r+P(i=e.year()-1,t,n):r>P(e.year(),t,n)?(s=r-P(e.year(),t,n),i=e.year()+1):(i=e.year(),s=r),{week:s,year:i}}function P(e,t,n){var s=ze(e,t,n),t=ze(e+1,t,n);return(Ae(e)-s+t)/7}s("w",["ww",2],"wo","week"),s("W",["WW",2],"Wo","isoWeek"),t("week","w"),t("isoWeek","W"),n("week",5),n("isoWeek",5),v("w",p),v("ww",p,w),v("W",p),v("WW",p,w),Te(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=g(e)});function Be(e,t){return e.slice(t,7).concat(e.slice(0,t))}s("d",0,"do","day"),s("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),s("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),s("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),s("e",0,0,"weekday"),s("E",0,0,"isoWeekday"),t("day","d"),t("weekday","e"),t("isoWeekday","E"),n("day",11),n("weekday",11),n("isoWeekday",11),v("d",p),v("e",p),v("E",p),v("dd",function(e,t){return t.weekdaysMinRegex(e)}),v("ddd",function(e,t){return t.weekdaysShortRegex(e)}),v("dddd",function(e,t){return t.weekdaysRegex(e)}),Te(["dd","ddd","dddd"],function(e,t,n,s){s=n._locale.weekdaysParse(e,s,n._strict);null!=s?t.d=s:m(n).invalidWeekday=e}),Te(["d","e","E"],function(e,t,n,s){t[s]=g(e)});var Je="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Qe="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Xe="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Ke=k,et=k,tt=k;function nt(){function e(e,t){return t.length-e.length}for(var t,n,s,i=[],r=[],a=[],o=[],u=0;u<7;u++)s=l([2e3,1]).day(u),t=M(this.weekdaysMin(s,"")),n=M(this.weekdaysShort(s,"")),s=M(this.weekdays(s,"")),i.push(t),r.push(n),a.push(s),o.push(t),o.push(n),o.push(s);i.sort(e),r.sort(e),a.sort(e),o.sort(e),this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+r.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function st(){return this.hours()%12||12}function it(e,t){s(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function rt(e,t){return t._meridiemParse}s("H",["HH",2],0,"hour"),s("h",["hh",2],0,st),s("k",["kk",2],0,function(){return this.hours()||24}),s("hmm",0,0,function(){return""+st.apply(this)+r(this.minutes(),2)}),s("hmmss",0,0,function(){return""+st.apply(this)+r(this.minutes(),2)+r(this.seconds(),2)}),s("Hmm",0,0,function(){return""+this.hours()+r(this.minutes(),2)}),s("Hmmss",0,0,function(){return""+this.hours()+r(this.minutes(),2)+r(this.seconds(),2)}),it("a",!0),it("A",!1),t("hour","h"),n("hour",13),v("a",rt),v("A",rt),v("H",p),v("h",p),v("k",p),v("HH",p,w),v("hh",p,w),v("kk",p,w),v("hmm",ge),v("hmmss",we),v("Hmm",ge),v("Hmmss",we),D(["H","HH"],x),D(["k","kk"],function(e,t,n){e=g(e);t[x]=24===e?0:e}),D(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),D(["h","hh"],function(e,t,n){t[x]=g(e),m(n).bigHour=!0}),D("hmm",function(e,t,n){var s=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s)),m(n).bigHour=!0}),D("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s,2)),t[N]=g(e.substr(i)),m(n).bigHour=!0}),D("Hmm",function(e,t,n){var s=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s))}),D("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s,2)),t[N]=g(e.substr(i))});k=de("Hours",!0);var at,ot={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Ce,monthsShort:Ue,week:{dow:0,doy:6},weekdays:Je,weekdaysMin:Xe,weekdaysShort:Qe,meridiemParse:/[ap]\.?m?\.?/i},R={},ut={};function lt(e){return e&&e.toLowerCase().replace("_","-")}function ht(e){for(var t,n,s,i,r=0;r<e.length;){for(t=(i=lt(e[r]).split("-")).length,n=(n=lt(e[r+1]))?n.split("-"):null;0<t;){if(s=dt(i.slice(0,t).join("-")))return s;if(n&&n.length>=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s<n;s+=1)if(e[s]!==t[s])return s;return n}(i,n)>=t-1)break;t--}r++}return at}function dt(t){var e;if(void 0===R[t]&&"undefined"!=typeof module&&module&&module.exports&&null!=t.match("^[^/\\\\]*$"))try{e=at._abbr,require("./locale/"+t),ct(e)}catch(e){R[t]=null}return R[t]}function ct(e,t){return e&&((t=o(t)?mt(e):ft(e,t))?at=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),at._abbr}function ft(e,t){if(null===t)return delete R[e],null;var n,s=ot;if(t.abbr=e,null!=R[e])Q("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=R[e]._config;else if(null!=t.parentLocale)if(null!=R[t.parentLocale])s=R[t.parentLocale]._config;else{if(null==(n=dt(t.parentLocale)))return ut[t.parentLocale]||(ut[t.parentLocale]=[]),ut[t.parentLocale].push({name:e,config:t}),null;s=n._config}return R[e]=new K(X(s,t)),ut[e]&&ut[e].forEach(function(e){ft(e.name,e.config)}),ct(e),R[e]}function mt(e){var t;if(!(e=e&&e._locale&&e._locale._abbr?e._locale._abbr:e))return at;if(!a(e)){if(t=dt(e))return t;e=[e]}return ht(e)}function _t(e){var t=e._a;return t&&-2===m(e).overflow&&(t=t[O]<0||11<t[O]?O:t[b]<1||t[b]>We(t[Y],t[O])?b:t[x]<0||24<t[x]||24===t[x]&&(0!==t[T]||0!==t[N]||0!==t[Ne])?x:t[T]<0||59<t[T]?T:t[N]<0||59<t[N]?N:t[Ne]<0||999<t[Ne]?Ne:-1,m(e)._overflowDayOfYear&&(t<Y||b<t)&&(t=b),m(e)._overflowWeeks&&-1===t&&(t=Pe),m(e)._overflowWeekday&&-1===t&&(t=Re),m(e).overflow=t),e}var yt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,wt=/Z|[+-]\d\d(?::?\d\d)?/,pt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],kt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],vt=/^\/?Date\((-?\d+)/i,Mt=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,Dt={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function St(e){var t,n,s,i,r,a,o=e._i,u=yt.exec(o)||gt.exec(o),o=pt.length,l=kt.length;if(u){for(m(e).iso=!0,t=0,n=o;t<n;t++)if(pt[t][1].exec(u[1])){i=pt[t][0],s=!1!==pt[t][2];break}if(null==i)e._isValid=!1;else{if(u[3]){for(t=0,n=l;t<n;t++)if(kt[t][1].exec(u[3])){r=(u[2]||" ")+kt[t][0];break}if(null==r)return void(e._isValid=!1)}if(s||null==r){if(u[4]){if(!wt.exec(u[4]))return void(e._isValid=!1);a="Z"}e._f=i+(r||"")+(a||""),Tt(e)}else e._isValid=!1}}else e._isValid=!1}function Yt(e,t,n,s,i,r){e=[function(e){e=parseInt(e,10);{if(e<=49)return 2e3+e;if(e<=999)return 1900+e}return e}(e),Ue.indexOf(t),parseInt(n,10),parseInt(s,10),parseInt(i,10)];return r&&e.push(parseInt(r,10)),e}function Ot(e){var t,n,s,i,r=Mt.exec(e._i.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,""));r?(t=Yt(r[4],r[3],r[2],r[5],r[6],r[7]),n=r[1],s=t,i=e,n&&Qe.indexOf(n)!==new Date(s[0],s[1],s[2]).getDay()?(m(i).weekdayMismatch=!0,i._isValid=!1):(e._a=t,e._tzm=(n=r[8],s=r[9],i=r[10],n?Dt[n]:s?0:60*(((n=parseInt(i,10))-(s=n%100))/100)+s),e._d=Ze.apply(null,e._a),e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),m(e).rfc2822=!0)):e._isValid=!1}function bt(e,t,n){return null!=e?e:null!=t?t:n}function xt(e){var t,n,s,i,r,a,o,u,l,h,d,c=[];if(!e._d){for(s=e,i=new Date(f.now()),n=s._useUTC?[i.getUTCFullYear(),i.getUTCMonth(),i.getUTCDate()]:[i.getFullYear(),i.getMonth(),i.getDate()],e._w&&null==e._a[b]&&null==e._a[O]&&(null!=(i=(s=e)._w).GG||null!=i.W||null!=i.E?(u=1,l=4,r=bt(i.GG,s._a[Y],qe(W(),1,4).year),a=bt(i.W,1),((o=bt(i.E,1))<1||7<o)&&(h=!0)):(u=s._locale._week.dow,l=s._locale._week.doy,d=qe(W(),u,l),r=bt(i.gg,s._a[Y],d.year),a=bt(i.w,d.week),null!=i.d?((o=i.d)<0||6<o)&&(h=!0):null!=i.e?(o=i.e+u,(i.e<0||6<i.e)&&(h=!0)):o=u),a<1||a>P(r,u,l)?m(s)._overflowWeeks=!0:null!=h?m(s)._overflowWeekday=!0:(d=$e(r,a,o,u,l),s._a[Y]=d.year,s._dayOfYear=d.dayOfYear)),null!=e._dayOfYear&&(i=bt(e._a[Y],n[Y]),(e._dayOfYear>Ae(i)||0===e._dayOfYear)&&(m(e)._overflowDayOfYear=!0),h=Ze(i,0,e._dayOfYear),e._a[O]=h.getUTCMonth(),e._a[b]=h.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=c[t]=n[t];for(;t<7;t++)e._a[t]=c[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[x]&&0===e._a[T]&&0===e._a[N]&&0===e._a[Ne]&&(e._nextDay=!0,e._a[x]=0),e._d=(e._useUTC?Ze:je).apply(null,c),r=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[x]=24),e._w&&void 0!==e._w.d&&e._w.d!==r&&(m(e).weekdayMismatch=!0)}}function Tt(e){if(e._f===f.ISO_8601)St(e);else if(e._f===f.RFC_2822)Ot(e);else{e._a=[],m(e).empty=!0;for(var t,n,s,i,r,a=""+e._i,o=a.length,u=0,l=ae(e._f,e._locale).match(te)||[],h=l.length,d=0;d<h;d++)n=l[d],(t=(a.match(Oe(n,e))||[])[0])&&(0<(s=a.substr(0,a.indexOf(t))).length&&m(e).unusedInput.push(s),a=a.slice(a.indexOf(t)+t.length),u+=t.length),ie[n]?(t?m(e).empty=!1:m(e).unusedTokens.push(n),s=n,r=e,null!=(i=t)&&c(xe,s)&&xe[s](i,r._a,r,s)):e._strict&&!t&&m(e).unusedTokens.push(n);m(e).charsLeftOver=o-u,0<a.length&&m(e).unusedInput.push(a),e._a[x]<=12&&!0===m(e).bigHour&&0<e._a[x]&&(m(e).bigHour=void 0),m(e).parsedDateParts=e._a.slice(0),m(e).meridiem=e._meridiem,e._a[x]=function(e,t,n){if(null==n)return t;return null!=e.meridiemHour?e.meridiemHour(t,n):null!=e.isPM?((e=e.isPM(n))&&t<12&&(t+=12),t=e||12!==t?t:0):t}(e._locale,e._a[x],e._meridiem),null!==(o=m(e).era)&&(e._a[Y]=e._locale.erasConvertYear(o,e._a[Y])),xt(e),_t(e)}}function Nt(e){var t,n,s,i=e._i,r=e._f;if(e._locale=e._locale||mt(e._l),null===i||void 0===r&&""===i)return I({nullInput:!0});if("string"==typeof i&&(e._i=i=e._locale.preparse(i)),h(i))return new q(_t(i));if(V(i))e._d=i;else if(a(r))!function(e){var t,n,s,i,r,a,o=!1,u=e._f.length;if(0===u)return m(e).invalidFormat=!0,e._d=new Date(NaN);for(i=0;i<u;i++)r=0,a=!1,t=$({},e),null!=e._useUTC&&(t._useUTC=e._useUTC),t._f=e._f[i],Tt(t),A(t)&&(a=!0),r=(r+=m(t).charsLeftOver)+10*m(t).unusedTokens.length,m(t).score=r,o?r<s&&(s=r,n=t):(null==s||r<s||a)&&(s=r,n=t,a&&(o=!0));E(e,n||t)}(e);else if(r)Tt(e);else if(o(r=(i=e)._i))i._d=new Date(f.now());else V(r)?i._d=new Date(r.valueOf()):"string"==typeof r?(n=i,null!==(t=vt.exec(n._i))?n._d=new Date(+t[1]):(St(n),!1===n._isValid&&(delete n._isValid,Ot(n),!1===n._isValid&&(delete n._isValid,n._strict?n._isValid=!1:f.createFromInputFallback(n))))):a(r)?(i._a=G(r.slice(0),function(e){return parseInt(e,10)}),xt(i)):F(r)?(t=i)._d||(s=void 0===(n=ue(t._i)).day?n.date:n.day,t._a=G([n.year,n.month,s,n.hour,n.minute,n.second,n.millisecond],function(e){return e&&parseInt(e,10)}),xt(t)):u(r)?i._d=new Date(r):f.createFromInputFallback(i);return A(e)||(e._d=null),e}function Pt(e,t,n,s,i){var r={};return!0!==t&&!1!==t||(s=t,t=void 0),!0!==n&&!1!==n||(s=n,n=void 0),(F(e)&&L(e)||a(e)&&0===e.length)&&(e=void 0),r._isAMomentObject=!0,r._useUTC=r._isUTC=i,r._l=n,r._i=e,r._f=t,r._strict=s,(i=new q(_t(Nt(i=r))))._nextDay&&(i.add(1,"d"),i._nextDay=void 0),i}function W(e,t,n,s){return Pt(e,t,n,s,!1)}f.createFromInputFallback=e("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(e){e._d=new Date(e._i+(e._useUTC?" UTC":""))}),f.ISO_8601=function(){},f.RFC_2822=function(){};ge=e("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=W.apply(null,arguments);return this.isValid()&&e.isValid()?e<this?this:e:I()}),we=e("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=W.apply(null,arguments);return this.isValid()&&e.isValid()?this<e?this:e:I()});function Rt(e,t){var n,s;if(!(t=1===t.length&&a(t[0])?t[0]:t).length)return W();for(n=t[0],s=1;s<t.length;++s)t[s].isValid()&&!t[s][e](n)||(n=t[s]);return n}var Wt=["year","quarter","month","week","day","hour","minute","second","millisecond"];function Ct(e){var e=ue(e),t=e.year||0,n=e.quarter||0,s=e.month||0,i=e.week||e.isoWeek||0,r=e.day||0,a=e.hour||0,o=e.minute||0,u=e.second||0,l=e.millisecond||0;this._isValid=function(e){var t,n,s=!1,i=Wt.length;for(t in e)if(c(e,t)&&(-1===S.call(Wt,t)||null!=e[t]&&isNaN(e[t])))return!1;for(n=0;n<i;++n)if(e[Wt[n]]){if(s)return!1;parseFloat(e[Wt[n]])!==g(e[Wt[n]])&&(s=!0)}return!0}(e),this._milliseconds=+l+1e3*u+6e4*o+1e3*a*60*60,this._days=+r+7*i,this._months=+s+3*n+12*t,this._data={},this._locale=mt(),this._bubble()}function Ut(e){return e instanceof Ct}function Ht(e){return e<0?-1*Math.round(-1*e):Math.round(e)}function Ft(e,n){s(e,0,0,function(){var e=this.utcOffset(),t="+";return e<0&&(e=-e,t="-"),t+r(~~(e/60),2)+n+r(~~e%60,2)})}Ft("Z",":"),Ft("ZZ",""),v("Z",Ye),v("ZZ",Ye),D(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Vt(Ye,e)});var Lt=/([\+\-]|\d\d)/gi;function Vt(e,t){var t=(t||"").match(e);return null===t?null:0===(t=60*(e=((t[t.length-1]||[])+"").match(Lt)||["-",0,0])[1]+g(e[2]))?0:"+"===e[0]?t:-t}function Gt(e,t){var n;return t._isUTC?(t=t.clone(),n=(h(e)||V(e)?e:W(e)).valueOf()-t.valueOf(),t._d.setTime(t._d.valueOf()+n),f.updateOffset(t,!1),t):W(e).local()}function Et(e){return-Math.round(e._d.getTimezoneOffset())}function At(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}f.updateOffset=function(){};var It=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,jt=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function C(e,t){var n,s=e,i=null;return Ut(e)?s={ms:e._milliseconds,d:e._days,M:e._months}:u(e)||!isNaN(+e)?(s={},t?s[t]=+e:s.milliseconds=+e):(i=It.exec(e))?(n="-"===i[1]?-1:1,s={y:0,d:g(i[b])*n,h:g(i[x])*n,m:g(i[T])*n,s:g(i[N])*n,ms:g(Ht(1e3*i[Ne]))*n}):(i=jt.exec(e))?(n="-"===i[1]?-1:1,s={y:Zt(i[2],n),M:Zt(i[3],n),w:Zt(i[4],n),d:Zt(i[5],n),h:Zt(i[6],n),m:Zt(i[7],n),s:Zt(i[8],n)}):null==s?s={}:"object"==typeof s&&("from"in s||"to"in s)&&(t=function(e,t){var n;if(!e.isValid()||!t.isValid())return{milliseconds:0,months:0};t=Gt(t,e),e.isBefore(t)?n=zt(e,t):((n=zt(t,e)).milliseconds=-n.milliseconds,n.months=-n.months);return n}(W(s.from),W(s.to)),(s={}).ms=t.milliseconds,s.M=t.months),i=new Ct(s),Ut(e)&&c(e,"_locale")&&(i._locale=e._locale),Ut(e)&&c(e,"_isValid")&&(i._isValid=e._isValid),i}function Zt(e,t){e=e&&parseFloat(e.replace(",","."));return(isNaN(e)?0:e)*t}function zt(e,t){var n={};return n.months=t.month()-e.month()+12*(t.year()-e.year()),e.clone().add(n.months,"M").isAfter(t)&&--n.months,n.milliseconds=+t-+e.clone().add(n.months,"M"),n}function $t(s,i){return function(e,t){var n;return null===t||isNaN(+t)||(Q(i,"moment()."+i+"(period, number) is deprecated. Please use moment()."+i+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),n=e,e=t,t=n),qt(this,C(e,t),s),this}}function qt(e,t,n,s){var i=t._milliseconds,r=Ht(t._days),t=Ht(t._months);e.isValid()&&(s=null==s||s,t&&Ve(e,ce(e,"Month")+t*n),r&&fe(e,"Date",ce(e,"Date")+r*n),i&&e._d.setTime(e._d.valueOf()+i*n),s&&f.updateOffset(e,r||t))}C.fn=Ct.prototype,C.invalid=function(){return C(NaN)};Ce=$t(1,"add"),Je=$t(-1,"subtract");function Bt(e){return"string"==typeof e||e instanceof String}function Jt(e){return h(e)||V(e)||Bt(e)||u(e)||function(t){var e=a(t),n=!1;e&&(n=0===t.filter(function(e){return!u(e)&&Bt(t)}).length);return e&&n}(e)||function(e){var t,n,s=F(e)&&!L(e),i=!1,r=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"],a=r.length;for(t=0;t<a;t+=1)n=r[t],i=i||c(e,n);return s&&i}(e)||null==e}function Qt(e,t){if(e.date()<t.date())return-Qt(t,e);var n=12*(t.year()-e.year())+(t.month()-e.month()),s=e.clone().add(n,"months"),t=t-s<0?(t-s)/(s-e.clone().add(n-1,"months")):(t-s)/(e.clone().add(1+n,"months")-s);return-(n+t)||0}function Xt(e){return void 0===e?this._locale._abbr:(null!=(e=mt(e))&&(this._locale=e),this)}f.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",f.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";Xe=e("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});function Kt(){return this._locale}var en=126227808e5;function tn(e,t){return(e%t+t)%t}function nn(e,t,n){return e<100&&0<=e?new Date(e+400,t,n)-en:new Date(e,t,n).valueOf()}function sn(e,t,n){return e<100&&0<=e?Date.UTC(e+400,t,n)-en:Date.UTC(e,t,n)}function rn(e,t){return t.erasAbbrRegex(e)}function an(){for(var e=[],t=[],n=[],s=[],i=this.eras(),r=0,a=i.length;r<a;++r)t.push(M(i[r].name)),e.push(M(i[r].abbr)),n.push(M(i[r].narrow)),s.push(M(i[r].name)),s.push(M(i[r].abbr)),s.push(M(i[r].narrow));this._erasRegex=new RegExp("^("+s.join("|")+")","i"),this._erasNameRegex=new RegExp("^("+t.join("|")+")","i"),this._erasAbbrRegex=new RegExp("^("+e.join("|")+")","i"),this._erasNarrowRegex=new RegExp("^("+n.join("|")+")","i")}function on(e,t){s(0,[e,e.length],0,t)}function un(e,t,n,s,i){var r;return null==e?qe(this,s,i).year:(r=P(e,s,i),function(e,t,n,s,i){e=$e(e,t,n,s,i),t=Ze(e.year,0,e.dayOfYear);return this.year(t.getUTCFullYear()),this.month(t.getUTCMonth()),this.date(t.getUTCDate()),this}.call(this,e,t=r<t?r:t,n,s,i))}s("N",0,0,"eraAbbr"),s("NN",0,0,"eraAbbr"),s("NNN",0,0,"eraAbbr"),s("NNNN",0,0,"eraName"),s("NNNNN",0,0,"eraNarrow"),s("y",["y",1],"yo","eraYear"),s("y",["yy",2],0,"eraYear"),s("y",["yyy",3],0,"eraYear"),s("y",["yyyy",4],0,"eraYear"),v("N",rn),v("NN",rn),v("NNN",rn),v("NNNN",function(e,t){return t.erasNameRegex(e)}),v("NNNNN",function(e,t){return t.erasNarrowRegex(e)}),D(["N","NN","NNN","NNNN","NNNNN"],function(e,t,n,s){s=n._locale.erasParse(e,s,n._strict);s?m(n).era=s:m(n).invalidEra=e}),v("y",Me),v("yy",Me),v("yyy",Me),v("yyyy",Me),v("yo",function(e,t){return t._eraYearOrdinalRegex||Me}),D(["y","yy","yyy","yyyy"],Y),D(["yo"],function(e,t,n,s){var i;n._locale._eraYearOrdinalRegex&&(i=e.match(n._locale._eraYearOrdinalRegex)),n._locale.eraYearOrdinalParse?t[Y]=n._locale.eraYearOrdinalParse(e,i):t[Y]=parseInt(e,10)}),s(0,["gg",2],0,function(){return this.weekYear()%100}),s(0,["GG",2],0,function(){return this.isoWeekYear()%100}),on("gggg","weekYear"),on("ggggg","weekYear"),on("GGGG","isoWeekYear"),on("GGGGG","isoWeekYear"),t("weekYear","gg"),t("isoWeekYear","GG"),n("weekYear",1),n("isoWeekYear",1),v("G",De),v("g",De),v("GG",p,w),v("gg",p,w),v("GGGG",ke,_e),v("gggg",ke,_e),v("GGGGG",ve,ye),v("ggggg",ve,ye),Te(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,s){t[s.substr(0,2)]=g(e)}),Te(["gg","GG"],function(e,t,n,s){t[s]=f.parseTwoDigitYear(e)}),s("Q",0,"Qo","quarter"),t("quarter","Q"),n("quarter",7),v("Q",i),D("Q",function(e,t){t[O]=3*(g(e)-1)}),s("D",["DD",2],"Do","date"),t("date","D"),n("date",9),v("D",p),v("DD",p,w),v("Do",function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient}),D(["D","DD"],b),D("Do",function(e,t){t[b]=g(e.match(p)[0])});ke=de("Date",!0);s("DDD",["DDDD",3],"DDDo","dayOfYear"),t("dayOfYear","DDD"),n("dayOfYear",4),v("DDD",pe),v("DDDD",me),D(["DDD","DDDD"],function(e,t,n){n._dayOfYear=g(e)}),s("m",["mm",2],0,"minute"),t("minute","m"),n("minute",14),v("m",p),v("mm",p,w),D(["m","mm"],T);var ln,_e=de("Minutes",!1),ve=(s("s",["ss",2],0,"second"),t("second","s"),n("second",15),v("s",p),v("ss",p,w),D(["s","ss"],N),de("Seconds",!1));for(s("S",0,0,function(){return~~(this.millisecond()/100)}),s(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),s(0,["SSS",3],0,"millisecond"),s(0,["SSSS",4],0,function(){return 10*this.millisecond()}),s(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),s(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),s(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),s(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),s(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),t("millisecond","ms"),n("millisecond",16),v("S",pe,i),v("SS",pe,w),v("SSS",pe,me),ln="SSSS";ln.length<=9;ln+="S")v(ln,Me);function hn(e,t){t[Ne]=g(1e3*("0."+e))}for(ln="S";ln.length<=9;ln+="S")D(ln,hn);ye=de("Milliseconds",!1),s("z",0,0,"zoneAbbr"),s("zz",0,0,"zoneName");i=q.prototype;function dn(e){return e}i.add=Ce,i.calendar=function(e,t){1===arguments.length&&(arguments[0]?Jt(arguments[0])?(e=arguments[0],t=void 0):function(e){for(var t=F(e)&&!L(e),n=!1,s=["sameDay","nextDay","lastDay","nextWeek","lastWeek","sameElse"],i=0;i<s.length;i+=1)n=n||c(e,s[i]);return t&&n}(arguments[0])&&(t=arguments[0],e=void 0):t=e=void 0);var e=e||W(),n=Gt(e,this).startOf("day"),n=f.calendarFormat(this,n)||"sameElse",t=t&&(d(t[n])?t[n].call(this,e):t[n]);return this.format(t||this.localeData().calendar(n,this,W(e)))},i.clone=function(){return new q(this)},i.diff=function(e,t,n){var s,i,r;if(!this.isValid())return NaN;if(!(s=Gt(e,this)).isValid())return NaN;switch(i=6e4*(s.utcOffset()-this.utcOffset()),t=_(t)){case"year":r=Qt(this,s)/12;break;case"month":r=Qt(this,s);break;case"quarter":r=Qt(this,s)/3;break;case"second":r=(this-s)/1e3;break;case"minute":r=(this-s)/6e4;break;case"hour":r=(this-s)/36e5;break;case"day":r=(this-s-i)/864e5;break;case"week":r=(this-s-i)/6048e5;break;default:r=this-s}return n?r:y(r)},i.endOf=function(e){var t,n;if(void 0===(e=_(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?sn:nn,e){case"year":t=n(this.year()+1,0,1)-1;break;case"quarter":t=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":t=n(this.year(),this.month()+1,1)-1;break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":t=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":t=this._d.valueOf(),t+=36e5-tn(t+(this._isUTC?0:6e4*this.utcOffset()),36e5)-1;break;case"minute":t=this._d.valueOf(),t+=6e4-tn(t,6e4)-1;break;case"second":t=this._d.valueOf(),t+=1e3-tn(t,1e3)-1;break}return this._d.setTime(t),f.updateOffset(this,!0),this},i.format=function(e){return e=e||(this.isUtc()?f.defaultFormatUtc:f.defaultFormat),e=re(this,e),this.localeData().postformat(e)},i.from=function(e,t){return this.isValid()&&(h(e)&&e.isValid()||W(e).isValid())?C({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},i.fromNow=function(e){return this.from(W(),e)},i.to=function(e,t){return this.isValid()&&(h(e)&&e.isValid()||W(e).isValid())?C({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},i.toNow=function(e){return this.to(W(),e)},i.get=function(e){return d(this[e=_(e)])?this[e]():this},i.invalidAt=function(){return m(this).overflow},i.isAfter=function(e,t){return e=h(e)?e:W(e),!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()>e.valueOf():e.valueOf()<this.clone().startOf(t).valueOf())},i.isBefore=function(e,t){return e=h(e)?e:W(e),!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()<e.valueOf():this.clone().endOf(t).valueOf()<e.valueOf())},i.isBetween=function(e,t,n,s){return e=h(e)?e:W(e),t=h(t)?t:W(t),!!(this.isValid()&&e.isValid()&&t.isValid())&&(("("===(s=s||"()")[0]?this.isAfter(e,n):!this.isBefore(e,n))&&(")"===s[1]?this.isBefore(t,n):!this.isAfter(t,n)))},i.isSame=function(e,t){var e=h(e)?e:W(e);return!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()===e.valueOf():(e=e.valueOf(),this.clone().startOf(t).valueOf()<=e&&e<=this.clone().endOf(t).valueOf()))},i.isSameOrAfter=function(e,t){return this.isSame(e,t)||this.isAfter(e,t)},i.isSameOrBefore=function(e,t){return this.isSame(e,t)||this.isBefore(e,t)},i.isValid=function(){return A(this)},i.lang=Xe,i.locale=Xt,i.localeData=Kt,i.max=we,i.min=ge,i.parsingFlags=function(){return E({},m(this))},i.set=function(e,t){if("object"==typeof e)for(var n=function(e){var t,n=[];for(t in e)c(e,t)&&n.push({unit:t,priority:le[t]});return n.sort(function(e,t){return e.priority-t.priority}),n}(e=ue(e)),s=n.length,i=0;i<s;i++)this[n[i].unit](e[n[i].unit]);else if(d(this[e=_(e)]))return this[e](t);return this},i.startOf=function(e){var t,n;if(void 0===(e=_(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?sn:nn,e){case"year":t=n(this.year(),0,1);break;case"quarter":t=n(this.year(),this.month()-this.month()%3,1);break;case"month":t=n(this.year(),this.month(),1);break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":t=n(this.year(),this.month(),this.date());break;case"hour":t=this._d.valueOf(),t-=tn(t+(this._isUTC?0:6e4*this.utcOffset()),36e5);break;case"minute":t=this._d.valueOf(),t-=tn(t,6e4);break;case"second":t=this._d.valueOf(),t-=tn(t,1e3);break}return this._d.setTime(t),f.updateOffset(this,!0),this},i.subtract=Je,i.toArray=function(){var e=this;return[e.year(),e.month(),e.date(),e.hour(),e.minute(),e.second(),e.millisecond()]},i.toObject=function(){var e=this;return{years:e.year(),months:e.month(),date:e.date(),hours:e.hours(),minutes:e.minutes(),seconds:e.seconds(),milliseconds:e.milliseconds()}},i.toDate=function(){return new Date(this.valueOf())},i.toISOString=function(e){if(!this.isValid())return null;var t=(e=!0!==e)?this.clone().utc():this;return t.year()<0||9999<t.year()?re(t,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):d(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",re(t,"Z")):re(t,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},i.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e,t="moment",n="";return this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",n="Z"),t="["+t+'("]',e=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",this.format(t+e+"-MM-DD[T]HH:mm:ss.SSS"+(n+'[")]'))},"undefined"!=typeof Symbol&&null!=Symbol.for&&(i[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),i.toJSON=function(){return this.isValid()?this.toISOString():null},i.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},i.unix=function(){return Math.floor(this.valueOf()/1e3)},i.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},i.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},i.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].name;if(t[n].until<=e&&e<=t[n].since)return t[n].name}return""},i.eraNarrow=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].narrow;if(t[n].until<=e&&e<=t[n].since)return t[n].narrow}return""},i.eraAbbr=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].abbr;if(t[n].until<=e&&e<=t[n].since)return t[n].abbr}return""},i.eraYear=function(){for(var e,t,n=this.localeData().eras(),s=0,i=n.length;s<i;++s)if(e=n[s].since<=n[s].until?1:-1,t=this.clone().startOf("day").valueOf(),n[s].since<=t&&t<=n[s].until||n[s].until<=t&&t<=n[s].since)return(this.year()-f(n[s].since).year())*e+n[s].offset;return this.year()},i.year=Ie,i.isLeapYear=function(){return he(this.year())},i.weekYear=function(e){return un.call(this,e,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},i.isoWeekYear=function(e){return un.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},i.quarter=i.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},i.month=Ge,i.daysInMonth=function(){return We(this.year(),this.month())},i.week=i.weeks=function(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")},i.isoWeek=i.isoWeeks=function(e){var t=qe(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")},i.weeksInYear=function(){var e=this.localeData()._week;return P(this.year(),e.dow,e.doy)},i.weeksInWeekYear=function(){var e=this.localeData()._week;return P(this.weekYear(),e.dow,e.doy)},i.isoWeeksInYear=function(){return P(this.year(),1,4)},i.isoWeeksInISOWeekYear=function(){return P(this.isoWeekYear(),1,4)},i.date=ke,i.day=i.days=function(e){if(!this.isValid())return null!=e?this:NaN;var t,n,s=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(t=e,n=this.localeData(),e="string"!=typeof t?t:isNaN(t)?"number"==typeof(t=n.weekdaysParse(t))?t:null:parseInt(t,10),this.add(e-s,"d")):s},i.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")},i.isoWeekday=function(e){return this.isValid()?null!=e?(t=e,n=this.localeData(),n="string"==typeof t?n.weekdaysParse(t)%7||7:isNaN(t)?null:t,this.day(this.day()%7?n:n-7)):this.day()||7:null!=e?this:NaN;var t,n},i.dayOfYear=function(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")},i.hour=i.hours=k,i.minute=i.minutes=_e,i.second=i.seconds=ve,i.millisecond=i.milliseconds=ye,i.utcOffset=function(e,t,n){var s,i=this._offset||0;if(!this.isValid())return null!=e?this:NaN;if(null==e)return this._isUTC?i:Et(this);if("string"==typeof e){if(null===(e=Vt(Ye,e)))return this}else Math.abs(e)<16&&!n&&(e*=60);return!this._isUTC&&t&&(s=Et(this)),this._offset=e,this._isUTC=!0,null!=s&&this.add(s,"m"),i!==e&&(!t||this._changeInProgress?qt(this,C(e-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,f.updateOffset(this,!0),this._changeInProgress=null)),this},i.utc=function(e){return this.utcOffset(0,e)},i.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(Et(this),"m")),this},i.parseZone=function(){var e;return null!=this._tzm?this.utcOffset(this._tzm,!1,!0):"string"==typeof this._i&&(null!=(e=Vt(Se,this._i))?this.utcOffset(e):this.utcOffset(0,!0)),this},i.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?W(e).utcOffset():0,(this.utcOffset()-e)%60==0)},i.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},i.isLocal=function(){return!!this.isValid()&&!this._isUTC},i.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},i.isUtc=At,i.isUTC=At,i.zoneAbbr=function(){return this._isUTC?"UTC":""},i.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},i.dates=e("dates accessor is deprecated. Use date instead.",ke),i.months=e("months accessor is deprecated. Use month instead",Ge),i.years=e("years accessor is deprecated. Use year instead",Ie),i.zone=e("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?(this.utcOffset(e="string"!=typeof e?-e:e,t),this):-this.utcOffset()}),i.isDSTShifted=e("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e,t={};return $(t,this),(t=Nt(t))._a?(e=(t._isUTC?l:W)(t._a),this._isDSTShifted=this.isValid()&&0<function(e,t,n){for(var s=Math.min(e.length,t.length),i=Math.abs(e.length-t.length),r=0,a=0;a<s;a++)(n&&e[a]!==t[a]||!n&&g(e[a])!==g(t[a]))&&r++;return r+i}(t._a,e.toArray())):this._isDSTShifted=!1,this._isDSTShifted});w=K.prototype;function cn(e,t,n,s){var i=mt(),s=l().set(s,t);return i[n](s,e)}function fn(e,t,n){if(u(e)&&(t=e,e=void 0),e=e||"",null!=t)return cn(e,t,n,"month");for(var s=[],i=0;i<12;i++)s[i]=cn(e,i,n,"month");return s}function mn(e,t,n,s){t=("boolean"==typeof e?u(t)&&(n=t,t=void 0):(t=e,e=!1,u(n=t)&&(n=t,t=void 0)),t||"");var i,r=mt(),a=e?r._week.dow:0,o=[];if(null!=n)return cn(t,(n+a)%7,s,"day");for(i=0;i<7;i++)o[i]=cn(t,(i+a)%7,s,"day");return o}w.calendar=function(e,t,n){return d(e=this._calendar[e]||this._calendar.sameElse)?e.call(t,n):e},w.longDateFormat=function(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.match(te).map(function(e){return"MMMM"===e||"MM"===e||"DD"===e||"dddd"===e?e.slice(1):e}).join(""),this._longDateFormat[e])},w.invalidDate=function(){return this._invalidDate},w.ordinal=function(e){return this._ordinal.replace("%d",e)},w.preparse=dn,w.postformat=dn,w.relativeTime=function(e,t,n,s){var i=this._relativeTime[n];return d(i)?i(e,t,n,s):i.replace(/%d/i,e)},w.pastFuture=function(e,t){return d(e=this._relativeTime[0<e?"future":"past"])?e(t):e.replace(/%s/i,t)},w.set=function(e){var t,n;for(n in e)c(e,n)&&(d(t=e[n])?this[n]=t:this["_"+n]=t);this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},w.eras=function(e,t){for(var n,s=this._eras||mt("en")._eras,i=0,r=s.length;i<r;++i){switch(typeof s[i].since){case"string":n=f(s[i].since).startOf("day"),s[i].since=n.valueOf();break}switch(typeof s[i].until){case"undefined":s[i].until=1/0;break;case"string":n=f(s[i].until).startOf("day").valueOf(),s[i].until=n.valueOf();break}}return s},w.erasParse=function(e,t,n){var s,i,r,a,o,u=this.eras();for(e=e.toUpperCase(),s=0,i=u.length;s<i;++s)if(r=u[s].name.toUpperCase(),a=u[s].abbr.toUpperCase(),o=u[s].narrow.toUpperCase(),n)switch(t){case"N":case"NN":case"NNN":if(a===e)return u[s];break;case"NNNN":if(r===e)return u[s];break;case"NNNNN":if(o===e)return u[s];break}else if(0<=[r,a,o].indexOf(e))return u[s]},w.erasConvertYear=function(e,t){var n=e.since<=e.until?1:-1;return void 0===t?f(e.since).year():f(e.since).year()+(t-e.offset)*n},w.erasAbbrRegex=function(e){return c(this,"_erasAbbrRegex")||an.call(this),e?this._erasAbbrRegex:this._erasRegex},w.erasNameRegex=function(e){return c(this,"_erasNameRegex")||an.call(this),e?this._erasNameRegex:this._erasRegex},w.erasNarrowRegex=function(e){return c(this,"_erasNarrowRegex")||an.call(this),e?this._erasNarrowRegex:this._erasRegex},w.months=function(e,t){return e?(a(this._months)?this._months:this._months[(this._months.isFormat||He).test(t)?"format":"standalone"])[e.month()]:a(this._months)?this._months:this._months.standalone},w.monthsShort=function(e,t){return e?(a(this._monthsShort)?this._monthsShort:this._monthsShort[He.test(t)?"format":"standalone"])[e.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},w.monthsParse=function(e,t,n){var s,i;if(this._monthsParseExact)return function(e,t,n){var s,i,r,e=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],s=0;s<12;++s)r=l([2e3,s]),this._shortMonthsParse[s]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[s]=this.months(r,"").toLocaleLowerCase();return n?"MMM"===t?-1!==(i=S.call(this._shortMonthsParse,e))?i:null:-1!==(i=S.call(this._longMonthsParse,e))?i:null:"MMM"===t?-1!==(i=S.call(this._shortMonthsParse,e))||-1!==(i=S.call(this._longMonthsParse,e))?i:null:-1!==(i=S.call(this._longMonthsParse,e))||-1!==(i=S.call(this._shortMonthsParse,e))?i:null}.call(this,e,t,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),s=0;s<12;s++){if(i=l([2e3,s]),n&&!this._longMonthsParse[s]&&(this._longMonthsParse[s]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[s]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[s]||(i="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[s]=new RegExp(i.replace(".",""),"i")),n&&"MMMM"===t&&this._longMonthsParse[s].test(e))return s;if(n&&"MMM"===t&&this._shortMonthsParse[s].test(e))return s;if(!n&&this._monthsParse[s].test(e))return s}},w.monthsRegex=function(e){return this._monthsParseExact?(c(this,"_monthsRegex")||Ee.call(this),e?this._monthsStrictRegex:this._monthsRegex):(c(this,"_monthsRegex")||(this._monthsRegex=Le),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},w.monthsShortRegex=function(e){return this._monthsParseExact?(c(this,"_monthsRegex")||Ee.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(c(this,"_monthsShortRegex")||(this._monthsShortRegex=Fe),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},w.week=function(e){return qe(e,this._week.dow,this._week.doy).week},w.firstDayOfYear=function(){return this._week.doy},w.firstDayOfWeek=function(){return this._week.dow},w.weekdays=function(e,t){return t=a(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?"format":"standalone"],!0===e?Be(t,this._week.dow):e?t[e.day()]:t},w.weekdaysMin=function(e){return!0===e?Be(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin},w.weekdaysShort=function(e){return!0===e?Be(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort},w.weekdaysParse=function(e,t,n){var s,i;if(this._weekdaysParseExact)return function(e,t,n){var s,i,r,e=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],s=0;s<7;++s)r=l([2e3,1]).day(s),this._minWeekdaysParse[s]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[s]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[s]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===t?-1!==(i=S.call(this._weekdaysParse,e))?i:null:"ddd"===t?-1!==(i=S.call(this._shortWeekdaysParse,e))?i:null:-1!==(i=S.call(this._minWeekdaysParse,e))?i:null:"dddd"===t?-1!==(i=S.call(this._weekdaysParse,e))||-1!==(i=S.call(this._shortWeekdaysParse,e))||-1!==(i=S.call(this._minWeekdaysParse,e))?i:null:"ddd"===t?-1!==(i=S.call(this._shortWeekdaysParse,e))||-1!==(i=S.call(this._weekdaysParse,e))||-1!==(i=S.call(this._minWeekdaysParse,e))?i:null:-1!==(i=S.call(this._minWeekdaysParse,e))||-1!==(i=S.call(this._weekdaysParse,e))||-1!==(i=S.call(this._shortWeekdaysParse,e))?i:null}.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),s=0;s<7;s++){if(i=l([2e3,1]).day(s),n&&!this._fullWeekdaysParse[s]&&(this._fullWeekdaysParse[s]=new RegExp("^"+this.weekdays(i,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[s]=new RegExp("^"+this.weekdaysShort(i,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[s]=new RegExp("^"+this.weekdaysMin(i,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[s]||(i="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[s]=new RegExp(i.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[s].test(e))return s;if(n&&"ddd"===t&&this._shortWeekdaysParse[s].test(e))return s;if(n&&"dd"===t&&this._minWeekdaysParse[s].test(e))return s;if(!n&&this._weekdaysParse[s].test(e))return s}},w.weekdaysRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||nt.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(c(this,"_weekdaysRegex")||(this._weekdaysRegex=Ke),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},w.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||nt.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(c(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=et),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},w.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||nt.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(c(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=tt),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},w.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},w.meridiem=function(e,t,n){return 11<e?n?"pm":"PM":n?"am":"AM"},ct("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===g(e%100/10)?"th":1==t?"st":2==t?"nd":3==t?"rd":"th")}}),f.lang=e("moment.lang is deprecated. Use moment.locale instead.",ct),f.langData=e("moment.langData is deprecated. Use moment.localeData instead.",mt);var _n=Math.abs;function yn(e,t,n,s){t=C(t,n);return e._milliseconds+=s*t._milliseconds,e._days+=s*t._days,e._months+=s*t._months,e._bubble()}function gn(e){return e<0?Math.floor(e):Math.ceil(e)}function wn(e){return 4800*e/146097}function pn(e){return 146097*e/4800}function kn(e){return function(){return this.as(e)}}pe=kn("ms"),me=kn("s"),Ce=kn("m"),we=kn("h"),ge=kn("d"),Je=kn("w"),k=kn("M"),_e=kn("Q"),ve=kn("y");function vn(e){return function(){return this.isValid()?this._data[e]:NaN}}var ye=vn("milliseconds"),ke=vn("seconds"),Ie=vn("minutes"),w=vn("hours"),Mn=vn("days"),Dn=vn("months"),Sn=vn("years");var Yn=Math.round,On={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function bn(e,t,n,s){var i=C(e).abs(),r=Yn(i.as("s")),a=Yn(i.as("m")),o=Yn(i.as("h")),u=Yn(i.as("d")),l=Yn(i.as("M")),h=Yn(i.as("w")),i=Yn(i.as("y")),r=(r<=n.ss?["s",r]:r<n.s&&["ss",r])||a<=1&&["m"]||a<n.m&&["mm",a]||o<=1&&["h"]||o<n.h&&["hh",o]||u<=1&&["d"]||u<n.d&&["dd",u];return(r=(r=null!=n.w?r||h<=1&&["w"]||h<n.w&&["ww",h]:r)||l<=1&&["M"]||l<n.M&&["MM",l]||i<=1&&["y"]||["yy",i])[2]=t,r[3]=0<+e,r[4]=s,function(e,t,n,s,i){return i.relativeTime(t||1,!!n,e,s)}.apply(null,r)}var xn=Math.abs;function Tn(e){return(0<e)-(e<0)||+e}function Nn(){if(!this.isValid())return this.localeData().invalidDate();var e,t,n,s,i,r,a,o=xn(this._milliseconds)/1e3,u=xn(this._days),l=xn(this._months),h=this.asSeconds();return h?(e=y(o/60),t=y(e/60),o%=60,e%=60,n=y(l/12),l%=12,s=o?o.toFixed(3).replace(/\.?0+$/,""):"",i=Tn(this._months)!==Tn(h)?"-":"",r=Tn(this._days)!==Tn(h)?"-":"",a=Tn(this._milliseconds)!==Tn(h)?"-":"",(h<0?"-":"")+"P"+(n?i+n+"Y":"")+(l?i+l+"M":"")+(u?r+u+"D":"")+(t||e||o?"T":"")+(t?a+t+"H":"")+(e?a+e+"M":"")+(o?a+s+"S":"")):"P0D"}var U=Ct.prototype;return U.isValid=function(){return this._isValid},U.abs=function(){var e=this._data;return this._milliseconds=_n(this._milliseconds),this._days=_n(this._days),this._months=_n(this._months),e.milliseconds=_n(e.milliseconds),e.seconds=_n(e.seconds),e.minutes=_n(e.minutes),e.hours=_n(e.hours),e.months=_n(e.months),e.years=_n(e.years),this},U.add=function(e,t){return yn(this,e,t,1)},U.subtract=function(e,t){return yn(this,e,t,-1)},U.as=function(e){if(!this.isValid())return NaN;var t,n,s=this._milliseconds;if("month"===(e=_(e))||"quarter"===e||"year"===e)switch(t=this._days+s/864e5,n=this._months+wn(t),e){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(t=this._days+Math.round(pn(this._months)),e){case"week":return t/7+s/6048e5;case"day":return t+s/864e5;case"hour":return 24*t+s/36e5;case"minute":return 1440*t+s/6e4;case"second":return 86400*t+s/1e3;case"millisecond":return Math.floor(864e5*t)+s;default:throw new Error("Unknown unit "+e)}},U.asMilliseconds=pe,U.asSeconds=me,U.asMinutes=Ce,U.asHours=we,U.asDays=ge,U.asWeeks=Je,U.asMonths=k,U.asQuarters=_e,U.asYears=ve,U.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*g(this._months/12):NaN},U._bubble=function(){var e=this._milliseconds,t=this._days,n=this._months,s=this._data;return 0<=e&&0<=t&&0<=n||e<=0&&t<=0&&n<=0||(e+=864e5*gn(pn(n)+t),n=t=0),s.milliseconds=e%1e3,e=y(e/1e3),s.seconds=e%60,e=y(e/60),s.minutes=e%60,e=y(e/60),s.hours=e%24,t+=y(e/24),n+=e=y(wn(t)),t-=gn(pn(e)),e=y(n/12),n%=12,s.days=t,s.months=n,s.years=e,this},U.clone=function(){return C(this)},U.get=function(e){return e=_(e),this.isValid()?this[e+"s"]():NaN},U.milliseconds=ye,U.seconds=ke,U.minutes=Ie,U.hours=w,U.days=Mn,U.weeks=function(){return y(this.days()/7)},U.months=Dn,U.years=Sn,U.humanize=function(e,t){if(!this.isValid())return this.localeData().invalidDate();var n=!1,s=On;return"object"==typeof e&&(t=e,e=!1),"boolean"==typeof e&&(n=e),"object"==typeof t&&(s=Object.assign({},On,t),null!=t.s&&null==t.ss&&(s.ss=t.s-1)),e=this.localeData(),t=bn(this,!n,s,e),n&&(t=e.pastFuture(+this,t)),e.postformat(t)},U.toISOString=Nn,U.toString=Nn,U.toJSON=Nn,U.locale=Xt,U.localeData=Kt,U.toIsoString=e("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Nn),U.lang=Xe,s("X",0,0,"unix"),s("x",0,0,"valueOf"),v("x",De),v("X",/[+-]?\d+(\.\d{1,3})?/),D("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e))}),D("x",function(e,t,n){n._d=new Date(g(e))}),f.version="2.29.4",H=W,f.fn=i,f.min=function(){return Rt("isBefore",[].slice.call(arguments,0))},f.max=function(){return Rt("isAfter",[].slice.call(arguments,0))},f.now=function(){return Date.now?Date.now():+new Date},f.utc=l,f.unix=function(e){return W(1e3*e)},f.months=function(e,t){return fn(e,t,"months")},f.isDate=V,f.locale=ct,f.invalid=I,f.duration=C,f.isMoment=h,f.weekdays=function(e,t,n){return mn(e,t,n,"weekdays")},f.parseZone=function(){return W.apply(null,arguments).parseZone()},f.localeData=mt,f.isDuration=Ut,f.monthsShort=function(e,t){return fn(e,t,"monthsShort")},f.weekdaysMin=function(e,t,n){return mn(e,t,n,"weekdaysMin")},f.defineLocale=ft,f.updateLocale=function(e,t){var n,s;return null!=t?(s=ot,null!=R[e]&&null!=R[e].parentLocale?R[e].set(X(R[e]._config,t)):(t=X(s=null!=(n=dt(e))?n._config:s,t),null==n&&(t.abbr=e),(s=new K(t)).parentLocale=R[e],R[e]=s),ct(e)):null!=R[e]&&(null!=R[e].parentLocale?(R[e]=R[e].parentLocale,e===ct()&&ct(e)):null!=R[e]&&delete R[e]),R[e]},f.locales=function(){return ee(R)},f.weekdaysShort=function(e,t,n){return mn(e,t,n,"weekdaysShort")},f.normalizeUnits=_,f.relativeTimeRounding=function(e){return void 0===e?Yn:"function"==typeof e&&(Yn=e,!0)},f.relativeTimeThreshold=function(e,t){return void 0!==On[e]&&(void 0===t?On[e]:(On[e]=t,"s"===e&&(On.ss=t-1),!0))},f.calendarFormat=function(e,t){return(e=e.diff(t,"days",!0))<-6?"sameElse":e<-1?"lastWeek":e<0?"lastDay":e<1?"sameDay":e<2?"nextDay":e<7?"nextWeek":"sameElse"},f.prototype=i,f.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},f}); +//# sourceMappingURL=moment.min.js.map diff --git a/package.json.sample b/package.json.sample index 024eb7c119e39..93d6159d12ec0 100644 --- a/package.json.sample +++ b/package.json.sample @@ -17,7 +17,7 @@ "grunt-contrib-connect": "~3.0.0", "grunt-contrib-cssmin": "~4.0.0", "grunt-contrib-imagemin": "~4.0.0", - "grunt-contrib-jasmine": "~3.0.0", + "grunt-contrib-jasmine": "~4.0.0", "grunt-contrib-less": "~2.1.0", "grunt-contrib-watch": "~1.1.0", "grunt-eslint": "~24.0.0", diff --git a/pub/errors/processor.php b/pub/errors/processor.php index 334359b5e6419..0cb182700856c 100644 --- a/pub/errors/processor.php +++ b/pub/errors/processor.php @@ -20,6 +20,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * phpcs:ignoreFile */ +#[\AllowDynamicProperties] class Processor { const MAGE_ERRORS_LOCAL_XML = 'local.xml'; diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 75097b36115ce..84a0897ace10d 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Setup\Console\Command; use Magento\Deploy\Console\Command\App\ConfigImportCommand; @@ -31,7 +33,7 @@ class UpgradeCommand extends AbstractSetupCommand /** * Option to skip deletion of generated/code directory. */ - const INPUT_KEY_KEEP_GENERATED = 'keep-generated'; + public const INPUT_KEY_KEEP_GENERATED = 'keep-generated'; /** * Installer service factory. @@ -55,7 +57,7 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; - /* + /** * @var CacheInterface */ private $cache; @@ -142,8 +144,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $searchConfig = $this->searchConfigFactory->create(); $this->cache->clean(); $searchConfig->validateSearchEngine(); - $installer->removeUnusedTriggers(); $installer->installSchema($request); + $installer->removeUnusedTriggers(); $installer->installDataFixtures($request, true); if ($this->deploymentConfig->isAvailable()) { @@ -163,6 +165,7 @@ protected function execute(InputInterface $input, OutputInterface $output) '<info>Please re-run Magento compile command. Use the command "setup:di:compile"</info>' ); } + $output->writeln( "<info>Media files stored outside of 'Media Gallery Allowed' folders" . " will not be available to the media gallery.</info>" diff --git a/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php b/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php index a134da85c225c..69738724ef506 100644 --- a/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php +++ b/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php @@ -6,6 +6,8 @@ namespace Magento\Setup\Fixtures; +use Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory as CouponCollectionFactory; + /** * Fixture for generating coupon codes * @@ -37,23 +39,32 @@ class CouponCodesFixture extends Fixture */ private $couponCodeFactory; + /** + * @var CouponCollectionFactory + */ + private $couponCollectionFactory; + /** * Constructor * * @param FixtureModel $fixtureModel * @param \Magento\SalesRule\Model\RuleFactory|null $ruleFactory * @param \Magento\SalesRule\Model\CouponFactory|null $couponCodeFactory + * @param CouponCollectionFactory|null $couponCollectionFactory */ public function __construct( FixtureModel $fixtureModel, \Magento\SalesRule\Model\RuleFactory $ruleFactory = null, - \Magento\SalesRule\Model\CouponFactory $couponCodeFactory = null + \Magento\SalesRule\Model\CouponFactory $couponCodeFactory = null, + CouponCollectionFactory $couponCollectionFactory = null ) { parent::__construct($fixtureModel); $this->ruleFactory = $ruleFactory ?: $this->fixtureModel->getObjectManager() ->get(\Magento\SalesRule\Model\RuleFactory::class); $this->couponCodeFactory = $couponCodeFactory ?: $this->fixtureModel->getObjectManager() ->get(\Magento\SalesRule\Model\CouponFactory::class); + $this->couponCollectionFactory = $couponCollectionFactory ?: $this->fixtureModel->getObjectManager() + ->get(CouponCollectionFactory::class); } /** @@ -64,7 +75,9 @@ public function __construct( public function execute() { $this->fixtureModel->resetObjectManager(); - $this->couponCodesCount = $this->fixtureModel->getValue('coupon_codes', 0); + $requestedCouponsCount = (int) $this->fixtureModel->getValue('coupon_codes', 0); + $existedCouponsCount = $this->couponCollectionFactory->create()->getSize(); + $this->couponCodesCount = max(0, $requestedCouponsCount - $existedCouponsCount); if (!$this->couponCodesCount) { return; } diff --git a/setup/src/Magento/Setup/Model/AdminAccount.php b/setup/src/Magento/Setup/Model/AdminAccount.php index 87b113a2c4b34..e180d6d10f464 100644 --- a/setup/src/Magento/Setup/Model/AdminAccount.php +++ b/setup/src/Magento/Setup/Model/AdminAccount.php @@ -17,12 +17,12 @@ class AdminAccount /**#@+ * Data keys */ - const KEY_USER = 'admin-user'; - const KEY_PASSWORD = 'admin-password'; - const KEY_EMAIL = 'admin-email'; - const KEY_FIRST_NAME = 'admin-firstname'; - const KEY_LAST_NAME = 'admin-lastname'; - const KEY_PREFIX = 'db-prefix'; + public const KEY_USER = 'admin-user'; + public const KEY_PASSWORD = 'admin-password'; + public const KEY_EMAIL = 'admin-email'; + public const KEY_FIRST_NAME = 'admin-firstname'; + public const KEY_LAST_NAME = 'admin-lastname'; + public const KEY_PREFIX = 'db-prefix'; /**#@- */ /** @@ -154,8 +154,7 @@ private function trackPassword($adminId, $passwordHash) } /** - * Validates that the username and email both match the user, - * and that password exists and is different from user name. + * Validate that the username and email both match the user,and that password exists and is different from username. * * @return void * @throws \Exception If the username and email do not both match data provided to install @@ -164,12 +163,14 @@ private function trackPassword($adminId, $passwordHash) public function validateUserMatches() { if (empty($this->data[self::KEY_PASSWORD])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( '"Password" is required. Enter and try again.' ); } if (strcasecmp($this->data[self::KEY_PASSWORD], $this->data[self::KEY_USER]) == 0) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'Password cannot be the same as the user name.' ); @@ -191,6 +192,7 @@ public function validateUserMatches() if ((strcasecmp($email, $this->data[self::KEY_EMAIL]) == 0) && (strcasecmp($username, $this->data[self::KEY_USER]) != 0)) { // email matched but username did not + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'An existing user has the given email but different username. ' . 'Username and email both need to match an existing user or both be new.' @@ -199,6 +201,7 @@ public function validateUserMatches() if ((strcasecmp($username, $this->data[self::KEY_USER]) == 0) && (strcasecmp($email, $this->data[self::KEY_EMAIL]) != 0)) { // username matched but email did not + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'An existing user has the given username but different email. ' . 'Username and email both need to match an existing user or both be new.' @@ -236,30 +239,30 @@ private function saveAdminUserRole($adminId) } /** - * Gets the "Administrators" role id, the special role created by data fixture in Authorization module. + * Gets an administrators role id, the special role created by data fixture in Authorization module. * - * @return int The id of the Administrators role - * @throws \Exception If Administrators role not found or problem connecting with database. + * @return int The id of an administrators role + * @throws \Exception If an administrators role not found or problem connecting with database. */ private function retrieveAdministratorsRoleId() { - // Get Administrators role id to use as parent_id + // Get an administrators role id to use as parent_id $administratorsRoleData = [ 'parent_id' => 0, 'tree_level' => 1, 'role_type' => Group::ROLE_TYPE, 'user_id' => 0, 'user_type' => UserContextInterface::USER_TYPE_ADMIN, - 'role_name' => 'Administrators', ]; $result = $this->connection->fetchRow( 'SELECT * FROM ' . $this->getTableName('authorization_role') . ' ' . 'WHERE parent_id = :parent_id AND tree_level = :tree_level AND role_type = :role_type AND ' . - 'user_id = :user_id AND user_type = :user_type AND role_name = :role_name', + 'user_id = :user_id AND user_type = :user_type ORDER BY sort_order DESC', $administratorsRoleData ); if (empty($result)) { - throw new \Exception('No Administrators role was found, data fixture needs to be run'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('No administrators role was found, data fixture needs to be run'); } else { // Found at least one, use first return $result['role_id']; diff --git a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php index 4ecbfd3deebf8..dfd5294364c51 100644 --- a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php +++ b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php @@ -94,7 +94,6 @@ private function getCustomerTemplate() 'disable_auto_group_change' => '0', 'dob' => '12-10-1991', 'firstname' => 'Firstname', - 'gender' => 1, 'group_id' => '1', 'lastname' => 'Lastname', 'middlename' => '', diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php index bc748728447d0..90b0a60c51ee8 100644 --- a/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php @@ -9,6 +9,8 @@ use Magento\Framework\ObjectManager\ObjectManager; use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\ResourceModel\Coupon\Collection as CouponCollection; +use Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory as CouponCollectionFactory; use Magento\SalesRule\Model\Rule; use Magento\Setup\Fixtures\CartPriceRulesFixture; use Magento\Setup\Fixtures\CouponCodesFixture; @@ -43,6 +45,11 @@ class CouponCodesFixtureTest extends TestCase */ private $couponCodeFactoryMock; + /** + * @var CouponCollectionFactory|MockObject + */ + private $couponCollectionFactoryMock; + /** * setUp */ @@ -54,10 +61,12 @@ protected function setUp(): void \Magento\SalesRule\Model\CouponFactory::class, ['create'] ); + $this->couponCollectionFactoryMock = $this->createMock(CouponCollectionFactory::class); $this->model = new CouponCodesFixture( $this->fixtureModelMock, $this->ruleFactoryMock, - $this->couponCodeFactoryMock + $this->couponCodeFactoryMock, + $this->couponCollectionFactoryMock ); } @@ -66,6 +75,14 @@ protected function setUp(): void */ public function testExecute() { + $couponCollectionMock = $this->createMock(CouponCollection::class); + $this->couponCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($couponCollectionMock); + $couponCollectionMock->expects($this->once()) + ->method('getSize') + ->willReturn(0); + $websiteMock = $this->createMock(Website::class); $websiteMock->expects($this->once()) ->method('getId') @@ -127,6 +144,14 @@ public function testExecute() */ public function testNoFixtureConfigValue() { + $couponCollectionMock = $this->createMock(CouponCollection::class); + $this->couponCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($couponCollectionMock); + $couponCollectionMock->expects($this->once()) + ->method('getSize') + ->willReturn(0); + $ruleMock = $this->createMock(Rule::class); $ruleMock->expects($this->never())->method('save'); @@ -148,6 +173,28 @@ public function testNoFixtureConfigValue() $this->model->execute(); } + public function testFixtureAlreadyCreated() + { + $couponCollectionMock = $this->createMock(CouponCollection::class); + $this->couponCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($couponCollectionMock); + $couponCollectionMock->expects($this->once()) + ->method('getSize') + ->willReturn(1); + + $this->fixtureModelMock + ->expects($this->once()) + ->method('getValue') + ->willReturn(1); + + $this->fixtureModelMock->expects($this->never())->method('getObjectManager'); + $this->ruleFactoryMock->expects($this->never())->method('create'); + $this->couponCodeFactoryMock->expects($this->never())->method('create'); + + $this->model->execute(); + } + /** * testGetActionTitle */ diff --git a/setup/src/Magento/Setup/Test/Unit/Model/AdminAccountTest.php b/setup/src/Magento/Setup/Test/Unit/Model/AdminAccountTest.php index 4a267ac7f0d1a..8447829d00b06 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/AdminAccountTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/AdminAccountTest.php @@ -180,14 +180,13 @@ public function testSaveUserExistsNewAdminRole(): void 'SELECT * FROM ' . $this->prefix . 'authorization_role WHERE parent_id = :parent_id AND tree_level = :tree_level ' . 'AND role_type = :role_type AND user_id = :user_id ' . - 'AND user_type = :user_type AND role_name = :role_name', + 'AND user_type = :user_type ORDER BY sort_order DESC', [ 'parent_id' => 0, 'tree_level' => 1, 'role_type' => 'G', 'user_id' => 0, 'user_type' => 2, - 'role_name' => 'Administrators', ], null, $administratorRoleData @@ -298,14 +297,13 @@ public function testSaveNewUserNewAdminRole(): void 'SELECT * FROM ' . $this->prefix . 'authorization_role WHERE parent_id = :parent_id AND tree_level = :tree_level ' . 'AND role_type = :role_type AND user_id = :user_id ' . - 'AND user_type = :user_type AND role_name = :role_name', + 'AND user_type = :user_type ORDER BY sort_order DESC', [ 'parent_id' => 0, 'tree_level' => 1, 'role_type' => 'G', 'user_id' => 0, 'user_type' => 2, - 'role_name' => 'Administrators', ], null, $administratorRoleData @@ -375,7 +373,7 @@ public function testSaveExceptionEmailNotMatch(): void public function testSaveExceptionSpecialAdminRoleNotFound(): void { $this->expectException('Exception'); - $this->expectExceptionMessage('No Administrators role was found, data fixture needs to be run'); + $this->expectExceptionMessage('No administrators role was found, data fixture needs to be run'); $this->dbAdapter->expects($this->exactly(3))->method('fetchRow')->willReturn([]); $this->dbAdapter->expects($this->once())->method('lastInsertId')->willReturn(1);